Merge pull request #5156 from opf/feature/adapt_query_representer

adapt query representer
pull/5177/head
Markus Kahl 8 years ago committed by GitHub
commit 596e82ff2f
  1. 18
      app/controllers/api/experimental/concerns/v3_naming.rb
  2. 44
      app/helpers/queries_helper.rb
  3. 28
      app/models/queries/available_filters.rb
  4. 11
      app/models/queries/base_filter.rb
  5. 3
      app/models/queries/filter_serializer.rb
  6. 20
      app/models/queries/work_packages/filter/category_filter.rb
  7. 15
      app/models/queries/work_packages/filter/custom_field_filter.rb
  8. 20
      app/models/queries/work_packages/filter/group_filter.rb
  9. 10
      app/models/queries/work_packages/filter/principal_base_filter.rb
  10. 19
      app/models/queries/work_packages/filter/priority_filter.rb
  11. 10
      app/models/queries/work_packages/filter/project_filter.rb
  12. 10
      app/models/queries/work_packages/filter/role_filter.rb
  13. 20
      app/models/queries/work_packages/filter/status_filter.rb
  14. 20
      app/models/queries/work_packages/filter/subproject_filter.rb
  15. 10
      app/models/queries/work_packages/filter/type_filter.rb
  16. 10
      app/models/queries/work_packages/filter/version_filter.rb
  17. 23
      app/models/query.rb
  18. 167
      app/services/api/v3/parse_query_params_service.rb
  19. 55
      app/services/api/v3/update_query_from_v3_params_service.rb
  20. 49
      app/services/api/v3/work_package_collection_from_query_params_service.rb
  21. 155
      app/services/api/v3/work_package_collection_from_query_service.rb
  22. 84
      app/services/update_query_from_params_service.rb
  23. 3
      config/locales/en.yml
  24. 1088
      doc/apiv3/endpoints/queries.apib
  25. 4
      doc/apiv3/endpoints/work-packages.apib
  26. 19
      lib/api/decorators/single.rb
  27. 14
      lib/api/root.rb
  28. 14
      lib/api/utilities/property_name_converter.rb
  29. 38
      lib/api/utilities/property_name_converter_work_package_dummy.rb
  30. 47
      lib/api/utilities/query_filters_name_converter.rb
  31. 9
      lib/api/utilities/query_filters_name_converter_context.rb
  32. 47
      lib/api/utilities/wp_property_name_converter.rb
  33. 67
      lib/api/v3/queries/columns/query_column_representer.rb
  34. 66
      lib/api/v3/queries/columns/query_columns_api.rb
  35. 46
      lib/api/v3/queries/filters/query_filter_decorator.rb
  36. 96
      lib/api/v3/queries/filters/query_filter_instance_representer.rb
  37. 65
      lib/api/v3/queries/filters/query_filter_representer.rb
  38. 71
      lib/api/v3/queries/filters/query_filters_api.rb
  39. 67
      lib/api/v3/queries/group_bys/query_group_by_representer.rb
  40. 66
      lib/api/v3/queries/group_bys/query_group_bys_api.rb
  41. 63
      lib/api/v3/queries/operators/query_operator_representer.rb
  42. 57
      lib/api/v3/queries/operators/query_operators_api.rb
  43. 16
      lib/api/v3/queries/queries_api.rb
  44. 90
      lib/api/v3/queries/query_params_representer.rb
  45. 131
      lib/api/v3/queries/query_representer.rb
  46. 73
      lib/api/v3/queries/sort_bys/query_sort_by_representer.rb
  47. 68
      lib/api/v3/queries/sort_bys/query_sort_bys_api.rb
  48. 99
      lib/api/v3/queries/sort_bys/sort_by_decorator.rb
  49. 20
      lib/api/v3/utilities/path_helper.rb
  50. 210
      lib/api/v3/work_packages/work_package_list_helpers.rb
  51. 15
      lib/api/v3/work_packages/work_packages_api.rb
  52. 11
      lib/api/v3/work_packages/work_packages_by_project_api.rb
  53. 9
      spec/lib/api/utilities/property_name_converter_spec.rb
  54. 90
      spec/lib/api/v3/queries/columns/query_column_representer_spec.rb
  55. 111
      spec/lib/api/v3/queries/filters/query_filter_instance_representer_spec.rb
  56. 106
      spec/lib/api/v3/queries/filters/query_filter_representer_spec.rb
  57. 90
      spec/lib/api/v3/queries/group_bys/query_group_by_representer_spec.rb
  58. 66
      spec/lib/api/v3/queries/operators/query_operator_representer_spec.rb
  59. 373
      spec/lib/api/v3/queries/query_representer_spec.rb
  60. 130
      spec/lib/api/v3/queries/sort_bys/query_sort_by_representer_spec.rb
  61. 30
      spec/lib/api/v3/utilities/path_helper_spec.rb
  62. 6
      spec/models/queries/available_filters_spec.rb
  63. 25
      spec/models/queries/work_packages/filter/category_filter_spec.rb
  64. 2
      spec/models/queries/work_packages/filter/created_at_filter_spec.rb
  65. 104
      spec/models/queries/work_packages/filter/custom_field_filter_spec.rb
  66. 2
      spec/models/queries/work_packages/filter/due_date_filter_spec.rb
  67. 2
      spec/models/queries/work_packages/filter/estimated_hours_filter_spec.rb
  68. 24
      spec/models/queries/work_packages/filter/group_filter_spec.rb
  69. 24
      spec/models/queries/work_packages/filter/priority_filter_spec.rb
  70. 25
      spec/models/queries/work_packages/filter/project_filter_spec.rb
  71. 26
      spec/models/queries/work_packages/filter/responsible_filter_spec.rb
  72. 24
      spec/models/queries/work_packages/filter/role_filter_spec.rb
  73. 2
      spec/models/queries/work_packages/filter/start_date_filter_spec.rb
  74. 23
      spec/models/queries/work_packages/filter/status_filter_spec.rb
  75. 3
      spec/models/queries/work_packages/filter/subject_filter_spec.rb
  76. 27
      spec/models/queries/work_packages/filter/subproject_filter_spec.rb
  77. 25
      spec/models/queries/work_packages/filter/type_filter_spec.rb
  78. 2
      spec/models/queries/work_packages/filter/updated_at_filter_spec.rb
  79. 25
      spec/models/queries/work_packages/filter/version_filter_spec.rb
  80. 25
      spec/models/queries/work_packages/filter/watcher_filter_spec.rb
  81. 82
      spec/requests/api/v3/queries/columns/query_columns_resource_spec.rb
  82. 98
      spec/requests/api/v3/queries/filters/query_filters_resource_spec.rb
  83. 91
      spec/requests/api/v3/queries/group_bys/query_group_bys_resource_spec.rb
  84. 82
      spec/requests/api/v3/queries/operators/query_operators_resource_spec.rb
  85. 35
      spec/requests/api/v3/queries/query_resource_spec.rb
  86. 101
      spec/requests/api/v3/queries/sort_bys/query_sort_bys_resource_spec.rb
  87. 231
      spec/services/api/v3/parse_query_params_service_spec.rb
  88. 114
      spec/services/api/v3/update_query_from_v3_params_service_spec.rb
  89. 89
      spec/services/api/v3/work_package_collection_from_query_params_service_spec.rb
  90. 383
      spec/services/api/v3/work_package_collection_from_query_service_spec.rb
  91. 104
      spec/services/update_query_from_params_service.rb
  92. 16
      spec/support/queries/filters/shared_filter_examples.rb

@ -28,9 +28,13 @@
module Api::Experimental::Concerns::V3Naming
def v3_to_internal_name(string, append_id: true)
API::Utilities::PropertyNameConverter.to_ar_name(string,
context: context_dummy,
refer_to_ids: append_id)
if append_id
API::Utilities::QueryFiltersNameConverter.to_ar_name(string,
refer_to_ids: append_id)
else
API::Utilities::WpPropertyNameConverter.to_ar_name(string,
refer_to_ids: append_id)
end
end
def internal_to_v3_name(string)
@ -86,12 +90,4 @@ module Api::Experimental::Concerns::V3Naming
json_query
end
private
def context_dummy
# memorize the work package because
# initializing a work package queries the db
@context_dummy ||= API::Utilities::PropertyNameConverterQueryContext.new
end
end

@ -47,9 +47,9 @@ module QueriesHelper
def add_filter_from_params(query, filters: params)
query.filters = []
query.add_filters(
fields_from_params(query, filters),
operators_from_params(query, filters),
values_from_params(query, filters)
fields_from_params(filters),
operators_from_params(filters),
values_from_params(filters)
)
end
@ -113,11 +113,7 @@ module QueriesHelper
def column_names_from_params(params)
names = params[:c] || (params[:query] && params[:query][:column_names])
if names
context = WorkPackage.new
names.map { |name| converter.to_ar_name name, context: context }
end
names.map { |name| attribute_converter.to_ar_name name } if names
end
def visible_queries
@ -138,24 +134,24 @@ module QueriesHelper
params[:group_by] || params[:groupBy] || params[:g]
end
def fields_from_params(query, params)
fix_field_array(query, params[:fields] || params[:f]).compact
def fields_from_params(params)
fix_field_array(params[:fields] || params[:f]).compact
end
def operators_from_params(query, params)
fix_field_hash(query, params[:operators] || params[:op])
def operators_from_params(params)
fix_field_hash(params[:operators] || params[:op])
end
def values_from_params(query, params)
fix_field_hash(query, params[:values] || params[:v])
def values_from_params(params)
fix_field_hash(params[:values] || params[:v])
end
def fix_field_hash(query, field_hash)
def fix_field_hash(field_hash)
return nil if field_hash.nil?
names = field_hash.keys
entries = names
.zip(fix_field_array(query, names))
.zip(fix_field_array(names))
.select { |_name, field| field.present? }
.map { |name, field| [field, field_hash[name]] }
@ -178,22 +174,18 @@ module QueriesHelper
# @param field_names [Array] Field names as read from the params.
# @return [Array] Returns a list of fixed field names. The list may contain nil values
# for fields which could not be found.
def fix_field_array(query, field_names)
def fix_field_array(field_names)
return [] if field_names.nil?
available_keys = query.available_filters.map(&:name)
field_names
.map { |name| converter.to_ar_name name, context: converter_context, refer_to_ids: true }
.map { |name| available_keys.find { |k| name =~ /#{k}(s|_id)?$/ } }
.map { |name| filter_converter.to_ar_name name, refer_to_ids: true }
end
def converter
API::Utilities::PropertyNameConverter
def filter_converter
API::Utilities::QueryFiltersNameConverter
end
def converter_context
# memoize to reduce overhead of WorkPackage.new
@fix_field_array_wp ||= API::Utilities::PropertyNameConverterQueryContext.new
def attribute_converter
API::Utilities::WpPropertyNameConverter
end
end

@ -28,6 +28,22 @@
#++
module Queries::AvailableFilters
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def registered_filters
Queries::Register.filters[self]
end
def find_registered_filter(key)
registered_filters.detect do |f|
f.key === key.to_sym
end
end
end
def available_filters
uninitialized = registered_filters - already_initialized_filters
@ -71,9 +87,7 @@ module Queries::AvailableFilters
end
def find_registered_filter(key)
registered_filters.detect do |f|
f.key === key.to_sym
end
self.class.find_registered_filter(key)
end
def find_initialized_filter(key)
@ -86,15 +100,11 @@ module Queries::AvailableFilters
@already_initialized_filters ||= []
end
def registered_filters
@registered_filters ||= filter_register
end
def initialized_filters
@initialized_filters ||= []
end
def filter_register
Queries::Register.filters[self.class]
def registered_filters
self.class.registered_filters
end
end

@ -157,6 +157,17 @@ class Queries::BaseFilter
@values = Array(values).reject(&:blank?).map(&:to_s)
end
# Does the filter filter on other models, e.g. User, Status
def ar_object_filter?
false
end
# List of objects the value represents
# is empty if the filter does not filter on other AR objects
def value_objects
[]
end
protected
def validate_inclusion_of_operator

@ -29,6 +29,7 @@
module Queries::FilterSerializer
extend Queries::AvailableFilters
extend Queries::AvailableFilters::ClassMethods
def self.load(serialized_filter_hash)
return [] if serialized_filter_hash.nil?
@ -46,7 +47,7 @@ module Queries::FilterSerializer
YAML.dump ((filters || []).map(&:to_hash).reduce(:merge) || {}).stringify_keys
end
def self.filter_register
def self.registered_filters
Queries::Register.filters[Query]
end
end

@ -31,9 +31,7 @@ class Queries::WorkPackages::Filter::CategoryFilter <
Queries::WorkPackages::Filter::WorkPackageFilter
def allowed_values
@allowed_values ||= begin
project.categories.map { |s| [s.name, s.id.to_s] }
end
all_project_categories.map { |s| [s.name, s.id.to_s] }
end
def available?
@ -52,4 +50,20 @@ class Queries::WorkPackages::Filter::CategoryFilter <
def self.key
:category_id
end
def value_objects
int_values = values.map(&:to_i)
all_project_categories.select { |c| int_values.include?(c.id) }
end
def ar_object_filter?
true
end
private
def all_project_categories
@all_project_categories ||= project.categories
end
end

@ -110,6 +110,21 @@ class Queries::WorkPackages::Filter::CustomFieldFilter <
end
end
def ar_object_filter?
%w{user version}.include? custom_field.field_format
end
def value_objects
case custom_field.field_format
when 'user'
User.find(values)
when 'version'
Version.find(values)
else
super
end
end
private
def custom_field_valid

@ -29,9 +29,7 @@
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
all_groups.map { |g| [g.name, g.id.to_s] }
end
def available?
@ -53,4 +51,20 @@ class Queries::WorkPackages::Filter::GroupFilter < Queries::WorkPackages::Filter
def self.key
:member_of_group
end
def ar_object_filter?
true
end
def value_objects
value_ints = values.map(&:to_i)
all_groups.select { |g| value_ints.include?(g.id) }
end
private
def all_groups
@all_groups ||= ::Group.all
end
end

@ -33,6 +33,16 @@ class Queries::WorkPackages::Filter::PrincipalBaseFilter <
User.current.logged? || allowed_values.any?
end
def value_objects
prepared_values = values.map { |value| value == 'me' ? User.current : value }
Principal.find(prepared_values)
end
def ar_object_filter?
true
end
private
def me_value

@ -27,11 +27,10 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class Queries::WorkPackages::Filter::PriorityFilter < Queries::WorkPackages::Filter::WorkPackageFilter
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
priorities.map { |s| [s.name, s.id.to_s] }
end
def available?
@ -50,9 +49,19 @@ class Queries::WorkPackages::Filter::PriorityFilter < Queries::WorkPackages::Fil
:priority_id
end
def ar_object_filter?
true
end
def value_objects
value_ints = values.map(&:to_i)
priorities.select { |p| value_ints.include? p.id }
end
private
def priorities
IssuePriority.active
@priorities ||= IssuePriority.active
end
end

@ -56,6 +56,16 @@ class Queries::WorkPackages::Filter::ProjectFilter < Queries::WorkPackages::Filt
:project_id
end
def ar_object_filter?
true
end
def value_objects
value_ints = values.map(&:to_i)
visible_projects.select { |p| value_ints.include?(p.id) }
end
private
def visible_projects

@ -54,6 +54,16 @@ class Queries::WorkPackages::Filter::RoleFilter < Queries::WorkPackages::Filter:
:assigned_to_role
end
def ar_object_filter?
true
end
def value_objects
value_ints = values.map(&:to_i)
roles.select { |r| value_ints.include?(r.id) }
end
private
def roles

@ -29,9 +29,7 @@
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
all_statuses.map { |s| [s.name, s.id.to_s] }
end
def available?
@ -49,4 +47,20 @@ class Queries::WorkPackages::Filter::StatusFilter < Queries::WorkPackages::Filte
def self.key
:status_id
end
def value_objects
values_ids = values.map(&:to_i)
all_statuses.select { |status| values_ids.include?(status.id) }
end
def ar_object_filter?
true
end
private
def all_statuses
@all_statuses ||= Status.all
end
end

@ -31,14 +31,14 @@ 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] }
visible_subprojects.map { |s| [s.name, s.id.to_s] }
end
end
def available?
project &&
!project.leaf? &&
project.descendants.visible.exists?
visible_subprojects.exists?
end
def type
@ -56,4 +56,20 @@ class Queries::WorkPackages::Filter::SubprojectFilter <
def self.key
:subproject_id
end
def ar_object_filter?
true
end
def value_objects
value_ints = values.map(&:to_i)
visible_subprojects.select { |p| value_ints.include?(p.id) }
end
private
def visible_subprojects
@visible_subprojects ||= project.descendants.visible
end
end

@ -51,6 +51,16 @@ class Queries::WorkPackages::Filter::TypeFilter <
:type_id
end
def ar_object_filter?
true
end
def value_objects
value_ints = values.map(&:to_i)
types.select { |t| value_ints.include?(t.id) }
end
private
def types

@ -52,6 +52,16 @@ class Queries::WorkPackages::Filter::VersionFilter <
:fixed_version_id
end
def ar_object_filter?
true
end
def value_objects
value_ints = values.map(&:to_i)
versions.select { |v| value_ints.include?(v.id) }
end
private
def versions

@ -246,6 +246,10 @@ class Query < ActiveRecord::Base
filter
end
def filtered?
filters.any?
end
def normalized_name
name.parameterize.underscore
end
@ -268,6 +272,21 @@ class Query < ActiveRecord::Base
self.available_columns = v
end
def self.all_columns
WorkPackageCustomField
.includes(:translations)
.map { |cf| ::QueryCustomFieldColumn.new(cf) }
.concat(available_columns)
end
def self.groupable_columns
all_columns.select(&:groupable)
end
def self.sortable_columns
all_columns.select(&:sortable)
end
def self.add_available_column(column)
available_columns << column if column.is_a?(QueryColumn)
end
@ -360,6 +379,10 @@ class Query < ActiveRecord::Base
sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
end
def sorted?
sort_criteria.any?
end
# Returns the SQL sort order that should be prepended for grouping
def group_by_sort_order
if grouped? && (column = group_by_column)

@ -0,0 +1,167 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
class ParseQueryParamsService
def call(params)
parsed_params = {}
parsed_params[:group_by] = group_by_from_params(params)
error_result = with_service_error_on_json_parse_error do
parsed_params[:filters] = filters_from_params(params)
parsed_params[:sort_by] = sort_by_from_params(params)
end
return error_result if error_result
parsed_params[:columns] = columns_from_params(params)
parsed_params[:display_sums] = sums_from_params(params)
ServiceResult.new(success: true,
result: without_empty(parsed_params))
end
def group_by_from_params(params)
convert_attribute(params[:group_by] || params[:groupBy] || params[:g])
end
def sort_by_from_params(params)
return unless params[:sortBy]
parse_sorting_from_json(params[:sortBy])
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 filters_from_params(params)
return unless params[:filters]
filters = JSON.parse(params[:filters])
filters.each_with_object([]) do |filter, array|
attribute = filter.keys.first # there should only be one attribute per filter
operator = filter[attribute]['operator']
values = filter[attribute]['values']
ar_attribute = convert_filter_attribute attribute, append_id: true
internal_representation = { field: ar_attribute,
operator: operator,
values: values }
array << internal_representation
end
end
def columns_from_params(params)
columns = params[:columns] || params[:c] || params[:column_names]
return unless columns
columns.map do |column|
convert_attribute(column)
end
end
def sums_from_params(params)
if params[:showSums] == 'true'
true
elsif params[:showSums] == 'false'
false
end
end
##
# Maps given field names coming from the frontend to the actual names
# as expected by the query. This works slightly different to what happens
# in #column_names_from_params. For instance while they column name is
# :type the expected field name is :type_id.
#
# Examples:
# * status => status_id
# * progresssDone => done_ratio
# * assigned => assigned_to
# * customField1 => cf_1
#
# @param query [Query] Query for which to get the correct field names.
# @param field_names [Array] Field names as read from the params.
# @return [Array] Returns a list of fixed field names. The list may contain nil values
# for fields which could not be found.
def fix_field_array(field_names)
return [] if field_names.nil?
field_names
.map { |name| convert_attribute name, append_id: true }
end
def parse_sorting_from_json(json)
JSON.parse(json).map do |order|
attribute, direction = if order.is_a?(Array)
[order.first, order.last]
elsif order.is_a?(String)
order.split(':')
end
[convert_attribute(attribute), direction]
end
end
def convert_attribute(attribute, append_id: false)
::API::Utilities::WpPropertyNameConverter.to_ar_name(attribute,
refer_to_ids: append_id)
end
def convert_filter_attribute(attribute, append_id: false)
::API::Utilities::QueryFiltersNameConverter.to_ar_name(attribute,
refer_to_ids: append_id)
end
def with_service_error_on_json_parse_error
yield
nil
rescue ::JSON::ParserError => error
result = ServiceResult.new
result.errors.add(:base, error.message)
return result
end
def without_empty(hash)
hash.select { |_, v| v.present? || v == false }
end
end
end
end

@ -0,0 +1,55 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
class UpdateQueryFromV3ParamsService
def initialize(query, user)
self.query = query
self.current_user = user
end
def call(params)
parsed = ::API::V3::ParseQueryParamsService
.new
.call(params)
if parsed.success?
::UpdateQueryFromParamsService
.new(query, current_user)
.call(parsed.result)
else
parsed
end
end
attr_accessor :query,
:current_user
end
end
end

@ -0,0 +1,49 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
class WorkPackageCollectionFromQueryParamsService
def initialize(user)
self.current_user = user
end
def call(params = {})
query = Query.new(name: '_', project: params[:project], sort_criteria: [['parent', 'desc']])
WorkPackageCollectionFromQueryService
.new(query, current_user)
.call(params)
end
private
attr_accessor :current_user
end
end
end

@ -0,0 +1,155 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
class WorkPackageCollectionFromQueryService
include Utilities::PathHelper
def initialize(query, user)
self.query = query
self.current_user = user
end
def call(params = {})
update = UpdateQueryFromV3ParamsService
.new(query, current_user)
.call(params)
if update.success?
representer = results_to_representer(params)
ServiceResult.new(success: true, result: representer)
else
update
end
end
private
def results_to_representer(params)
collection_representer(query.results.sorted_work_packages,
params: params,
project: query.project,
groups: generate_groups,
sums: generate_total_sums)
end
attr_accessor :query,
:current_user
def representer
::API::V3::WorkPackages::WorkPackageCollectionRepresenter
end
def calculate_resulting_params(provided_params)
calculate_default_params
.merge(provided_params.slice('offset', 'pageSize').symbolize_keys)
end
def calculate_default_params
::API::V3::Queries::QueryParamsRepresenter
.new(query)
.to_h
end
def generate_groups
return unless query.grouped?
results = query.results
results.work_package_count_by_group.map do |group, count|
sums = if query.display_sums?
format_query_sums results.all_sums_for_group(group)
end
::API::Decorators::AggregationGroup.new(group, count, sums: sums)
end
end
def generate_total_sums
return unless query.display_sums?
format_query_sums query.results.all_total_sums
end
def format_query_sums(sums)
OpenStruct.new(format_column_keys(sums))
end
def format_column_keys(hash_by_column)
::Hash[
hash_by_column.map do |column, value|
match = /cf_(\d+)/.match(column.name.to_s)
column_name = if match
"custom_field_#{match[1]}"
else
column.name.to_s
end
[column_name, value]
end
]
end
def collection_representer(work_packages, params:, project:, groups:, sums:)
resulting_params = calculate_resulting_params(params)
::API::V3::WorkPackages::WorkPackageCollectionRepresenter.new(
work_packages,
self_link(project),
project: project,
query: resulting_params,
page: to_i_or_nil(resulting_params[:offset]),
per_page: to_i_or_nil(resulting_params[:pageSize]),
groups: groups,
total_sums: sums,
embed_schemas: true,
current_user: current_user
)
end
def to_i_or_nil(value)
value ? value.to_i : nil
end
def self_link(project)
if project
api_v3_paths.work_packages_by_project(project.id)
else
api_v3_paths.work_packages
end
end
def convert_to_v3(attribute)
::API::Utilities::PropertyNameConverter.from_ar_name(attribute).to_sym
end
end
end
end

@ -0,0 +1,84 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class UpdateQueryFromParamsService
def initialize(query, user)
self.query = query
self.current_user = user
end
def call(params)
apply_group_by(params)
apply_sort_by(params)
apply_filters(params)
apply_columns(params)
apply_sums(params)
if query.valid?
ServiceResult.new(success: true,
result: query)
else
ServiceResult.new(errors: query.errors)
end
end
private
def apply_group_by(params)
query.group_by = params[:group_by] if params[:group_by]
end
def apply_sort_by(params)
query.sort_criteria = params[:sort_by] if params[:sort_by]
end
def apply_filters(params)
return unless params[:filters]
query.filters = []
params[:filters].each do |filter|
query.add_filter(filter[:field], filter[:operator], filter[:values])
end
end
def apply_columns(params)
query.column_names = params[:columns] if params[:columns]
end
def apply_sums(params)
query.display_sums = params[:display_sums] if params[:display_sums]
end
attr_accessor :query,
:current_user,
:params
end

@ -1639,6 +1639,9 @@ en:
project_module_timelines: "Timelines"
project_module_wiki: "Wiki"
query:
attribute_and_direction: "%{attribute} (%{direction})"
# possible query parameters (e.g. issue queries),
# which are not attributes of an AR-Model.
query_fields:

File diff suppressed because it is too large Load Diff

@ -609,7 +609,7 @@ For more details and all possible responses see the general specification of [Fo
+ groupBy (optional, string, `status`) ... The column to group by.
+ showSums = `false` (optional), boolean, `true` ... Indicates whether properties should be summed up if they support it.
+ showSums = `false` (optional, boolean, `true`) ... Indicates whether properties should be summed up if they support it.
+ Response 200 (application/hal+json)
@ -795,7 +795,7 @@ A project link must be set when creating work packages through this route.
+ groupBy (optional, string, `status`) ... The column to group by.
+ showSums = `false` (optional), boolean, `true` ... Indicates whether properties should be summed up if they support it.
+ showSums = `false` (optional, boolean, `true`) ... Indicates whether properties should be summed up if they support it.
+ Response 200 (application/hal+json)

@ -65,8 +65,9 @@ module API
def self.self_link(path: nil, id_attribute: :id, title_getter: -> (*) { represented.name })
link :self do
path = _type.underscore unless path
link_object = { href: api_v3_paths.send(path, represented.send(id_attribute)) }
self_path = self_v3_path(path, id_attribute)
link_object = { href: self_path }
title = instance_eval(&title_getter)
link_object[:title] = title if title
@ -151,6 +152,20 @@ module API
def model_required?
true
end
def self_v3_path(path, id_attribute)
path = _type.underscore unless path
id = if id_attribute.respond_to?(:call)
instance_eval(&id_attribute)
else
represented.send(id_attribute)
end
id = [nil] if id.nil?
api_v3_paths.send(path, *Array(id))
end
end
end
end

@ -148,6 +148,20 @@ module API
raise API::Errors::Unauthorized unless authorized
authorized
end
def raise_invalid_query_on_service_failure
service = yield
if service.success?
service
else
api_errors = service.errors.full_messages.map do |message|
::API::Errors::InvalidQuery.new(message)
end
raise ::API::Errors::MultipleErrors.create_if_many api_errors
end
end
end
def self.auth_headers

@ -84,12 +84,18 @@ module API
attribute = collapse_custom_field_name(attribute)
special_conversion = special_api_to_ar_conversions[attribute]
if special_conversion && context.respond_to?(special_conversion)
attribute = special_conversion
if refer_to_ids
special_conversion = denormalize_foreign_key_name(special_conversion, context)
end
attribute = denormalize_foreign_key_name(attribute, context) if refer_to_ids
attribute
if special_conversion && context.respond_to?(special_conversion)
special_conversion
elsif refer_to_ids
denormalize_foreign_key_name(attribute, context)
else
attribute
end
end
private

@ -0,0 +1,38 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
# The sole purpose of this is to have a work package
# that is inexpensive to initialize by overriding the after_initialize hook
module API
module Utilities
class PropertyNameConverterWorkPackageDummy < ::WorkPackage
def set_default_values; end
end
end
end

@ -0,0 +1,47 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'api/utilities/property_name_converter'
require 'api/utilities/query_filters_name_converter_context'
module API
module Utilities
class QueryFiltersNameConverter
class << self
def to_ar_name(attribute, refer_to_ids: false)
conversion_wp = ::API::Utilities::QueryFiltersNameConverterContext.new
::API::Utilities::PropertyNameConverter.to_ar_name(attribute,
context: conversion_wp,
refer_to_ids: refer_to_ids)
end
end
end
end
end

@ -0,0 +1,9 @@
module API
module Utilities
class QueryFiltersNameConverterContext
def respond_to?(method_name, include_private = false)
Query.registered_filters.map(&:key).include?(method_name.to_sym) || super
end
end
end
end

@ -0,0 +1,47 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'api/utilities/property_name_converter'
require 'api/utilities/property_name_converter_work_package_dummy'
module API
module Utilities
class WpPropertyNameConverter
class << self
def to_ar_name(attribute, refer_to_ids: false)
conversion_wp = ::API::Utilities::PropertyNameConverterWorkPackageDummy.new
::API::Utilities::PropertyNameConverter.to_ar_name(attribute,
context: conversion_wp,
refer_to_ids: refer_to_ids)
end
end
end
end
end

@ -0,0 +1,67 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Queries
module Columns
class QueryColumnRepresenter < ::API::Decorators::Single
self_link id_attribute: ->(*) { converted_name },
title_getter: ->(*) { represented.caption }
def initialize(model)
super(model, current_user: nil, embed_links: true)
end
property :id,
exec_context: :decorator
property :caption,
as: :name
private
def converted_name
convert_attribute(represented.name)
end
alias :id :converted_name
def _type
'QueryColumn'
end
def convert_attribute(attribute)
::API::Utilities::PropertyNameConverter.from_ar_name(attribute)
end
end
end
end
end
end

@ -0,0 +1,66 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Queries
module Columns
class QueryColumnsAPI < ::API::OpenProjectAPI
resource :columns do
helpers do
def convert_to_ar(attribute)
::API::Utilities::WpPropertyNameConverter.to_ar_name(attribute)
end
end
params do
requires :id, desc: 'Column id'
end
before do
authorize(:view_work_packages, global: true, user: current_user)
end
route_param :id do
get do
ar_id = convert_to_ar(params[:id]).to_sym
column = Query.all_columns.detect { |candidate| candidate.name == ar_id }
if column
::API::V3::Queries::Columns::QueryColumnRepresenter.new(column)
else
raise API::Errors::NotFound
end
end
end
end
end
end
end
end
end

@ -0,0 +1,46 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Queries
module Filters
class QueryFilterDecorator
def initialize(filter)
self.filter = filter
end
private
attr_accessor :filter
end
end
end
end
end

@ -0,0 +1,96 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Queries
module Filters
class QueryFilterInstanceRepresenter < ::API::Decorators::Single
def initialize(model)
super(model, current_user: nil, embed_links: true)
end
link :filter do
{
href: api_v3_paths.query_filter(converted_name),
title: name
}
end
link :operator do
{
href: api_v3_paths.query_operator(represented.operator),
title: operator_name
}
end
links :values do
next unless represented.ar_object_filter?
represented.value_objects.map do |value_object|
{
href: api_v3_paths.send(value_object.class.name.downcase, value_object.id),
title: value_object.name
}
end
end
property :name,
exec_context: :decorator
property :values,
if: ->(*) { !ar_object_filter? },
show_nil: true
private
def name
represented.human_name
end
def _type
"#{converted_name.camelize}QueryFilter"
end
def converted_name
convert_attribute(represented.name)
end
def operator_name
I18n.t(represented.class.operators[represented.operator.to_sym])
end
def convert_attribute(attribute)
::API::Utilities::PropertyNameConverter.from_ar_name(attribute)
end
end
end
end
end
end

@ -0,0 +1,65 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Queries
module Filters
class QueryFilterRepresenter < ::API::Decorators::Single
self_link id_attribute: ->(*) { converted_key },
title_getter: ->(*) { represented.human_name },
path: :query_filter
def initialize(model)
super(model, current_user: nil, embed_links: true)
end
property :id,
exec_context: :decorator
private
def converted_key
convert_attribute(represented.name)
end
alias :id :converted_key
def _type
'QueryFilter'
end
def convert_attribute(attribute)
::API::Utilities::PropertyNameConverter.from_ar_name(attribute)
end
end
end
end
end
end

@ -0,0 +1,71 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Queries
module Filters
class QueryFiltersAPI < ::API::OpenProjectAPI
resource :filters do
helpers do
def convert_to_ar(attribute)
::API::Utilities::QueryFiltersNameConverter.to_ar_name(attribute,
refer_to_ids: true)
end
end
params do
requires :id, desc: 'Filter id'
end
before do
authorize(:view_work_packages, global: true, user: current_user)
end
route_param :id do
get do
ar_id = convert_to_ar(params[:id])
filter_class = Query.find_registered_filter(ar_id)
if filter_class
filter = filter_class.new
filter.name = ar_id
::API::V3::Queries::Filters::QueryFilterRepresenter.new(filter)
else
raise API::Errors::NotFound
end
end
end
end
end
end
end
end
end

@ -0,0 +1,67 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Queries
module GroupBys
class QueryGroupByRepresenter < ::API::Decorators::Single
self_link id_attribute: ->(*) { converted_name },
title_getter: ->(*) { represented.caption }
def initialize(model)
super(model, current_user: nil, embed_links: true)
end
property :id,
exec_context: :decorator
property :caption,
as: :name
private
def converted_name
convert_attribute(represented.name)
end
alias :id :converted_name
def _type
'QueryGroupBy'
end
def convert_attribute(attribute)
::API::Utilities::PropertyNameConverter.from_ar_name(attribute)
end
end
end
end
end
end

@ -0,0 +1,66 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Queries
module GroupBys
class QueryGroupBysAPI < ::API::OpenProjectAPI
resource :group_bys do
helpers do
def convert_to_ar(attribute)
::API::Utilities::WpPropertyNameConverter.to_ar_name(attribute)
end
end
params do
requires :id, desc: 'Group by id'
end
before do
authorize(:view_work_packages, global: true, user: current_user)
end
route_param :id do
get do
ar_id = convert_to_ar(params[:id]).to_sym
column = Query.groupable_columns.detect { |candidate| candidate.name == ar_id }
if column
::API::V3::Queries::GroupBys::QueryGroupByRepresenter.new(column)
else
raise API::Errors::NotFound
end
end
end
end
end
end
end
end
end

@ -0,0 +1,63 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Queries
module Operators
class QueryOperatorRepresenter < ::API::Decorators::Single
self_link id_attribute: ->(*) { represented },
title_getter: ->(*) { name }
def initialize(model)
super(model.to_sym, current_user: nil, embed_links: true)
end
property :id,
exec_context: :decorator
property :name,
exec_context: :decorator
private
def name
I18n.t(::Queries::BaseFilter.operators[represented])
end
alias :id :represented
def _type
'QueryOperator'
end
end
end
end
end
end

@ -0,0 +1,57 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Queries
module Operators
class QueryOperatorsAPI < ::API::OpenProjectAPI
resource :operators do
params do
requires :id, desc: 'Operator id'
end
before do
authorize(:view_work_packages, global: true, user: current_user)
end
route_param :id do
get do
if ::Queries::BaseFilter.operators[params[:id].to_sym]
::API::V3::Queries::Operators::QueryOperatorRepresenter.new(params[:id])
else
raise API::Errors::NotFound
end
end
end
end
end
end
end
end
end

@ -34,6 +34,12 @@ module API
module Queries
class QueriesAPI < ::API::OpenProjectAPI
resources :queries do
mount API::V3::Queries::Columns::QueryColumnsAPI
mount API::V3::Queries::GroupBys::QueryGroupBysAPI
mount API::V3::Queries::SortBys::QuerySortBysAPI
mount API::V3::Queries::Filters::QueryFiltersAPI
mount API::V3::Queries::Operators::QueryOperatorsAPI
get do
authorize_any [:view_work_packages, :manage_public_queries], global: true
@ -58,7 +64,15 @@ module API
route_param :id do
before do
@query = Query.find(params[:id])
@representer = QueryRepresenter.new(@query, current_user: current_user)
results_representer = ::API::V3::WorkPackageCollectionFromQueryService
.new(@query, current_user)
.call(params)
@representer = QueryRepresenter.new(@query,
current_user: current_user,
results: results_representer.result,
params: params)
authorize_by_policy(:show) do
raise API::Errors::NotFound
end

@ -0,0 +1,90 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
# Other than the Roar based representers of the api v3, this
# representer is only responsible for transforming a query's
# attributes into a hash which in turn can be used e.g. to be displayed
# in a url
module API
module V3
module Queries
class QueryParamsRepresenter
def initialize(query)
self.query = query
end
def to_h
p = default_hash
p[:showSums] = 'true' if query.display_sums?
p[:groupBy] = query.group_by if query.group_by?
p[:sortBy] = sort_criteria_to_v3 if query.sorted?
p[:filters] = filters_to_v3 if query.filtered?
p
end
def self_link
if query.project
api_v3_paths.work_packages_by_project(query.project.id)
else
api_v3_paths.work_packages
end
end
private
def sort_criteria_to_v3
converted = query.sort_criteria.map { |first, last| [convert_to_v3(first), last] }
JSON::dump(converted)
end
def filters_to_v3
converted = query.filters.map do |filter|
{ convert_to_v3(filter.name) => { operator: filter.operator, values: filter.values } }
end
JSON::dump(converted)
end
def convert_to_v3(attribute)
::API::Utilities::PropertyNameConverter.from_ar_name(attribute).to_sym
end
def default_hash
{ offset: 1, pageSize: Setting.per_page_options_array.first }
end
attr_accessor :query
end
end
end
end

@ -36,6 +36,71 @@ module API
class QueryRepresenter < ::API::Decorators::Single
self_link
attr_accessor :results,
:params
def initialize(model,
current_user:,
results: nil,
embed_links: false,
params: {})
self.results = results
self.params = params
super(model, current_user: current_user, embed_links: embed_links)
end
link :results do
path = if represented.project
api_v3_paths.work_packages_by_project(represented.project.id)
else
api_v3_paths.work_packages
end
url_query = ::API::V3::Queries::QueryParamsRepresenter
.new(represented)
.to_h
.merge(params.slice(:offset, :pageSize))
{
href: [path, url_query.to_query].join('?')
}
end
links :columns do
represented.columns.map do |column|
{
href: api_v3_paths.query_column(convert_attribute(column.name).underscore),
title: column.caption
}
end
end
link :groupBy do
column = represented.group_by_column
if column
{
href: api_v3_paths.query_group_by(convert_attribute(column.name).underscore),
title: column.caption
}
else
{
href: nil,
title: nil
}
end
end
links :sortBy do
map_with_sort_by_as_decorated do |sort_by|
{
href: api_v3_paths.query_sort_by(sort_by.converted_name, sort_by.direction_name),
title: sort_by.name
}
end
end
linked_property :user
linked_property :project
@ -45,35 +110,62 @@ module API
exec_context: :decorator,
getter: ->(*) {
represented.filters.map do |filter|
attribute = convert_attribute filter.field
{
attribute => { operator: filter.operator, values: filter.values }
}
::API::V3::Queries::Filters::QueryFilterInstanceRepresenter.new(filter)
end
}
property :is_public, getter: -> (*) { is_public }
property :column_names,
property :public, getter: -> (*) { is_public }
property :sort_by,
exec_context: :decorator,
getter: ->(*) {
return nil unless represented.column_names
represented.column_names.map { |name| convert_attribute name }
return unless represented.sort_criteria
map_with_sort_by_as_decorated do |sort_by|
::API::V3::Queries::SortBys::QuerySortByRepresenter.new(sort_by)
end
},
embedded: true,
if: ->(*) {
embed_links
}
property :sort_criteria,
property :sums, getter: -> (*) { display_sums }
property :starred, getter: -> (*) { starred }
property :columns,
exec_context: :decorator,
getter: ->(*) {
return nil unless represented.sort_criteria
represented.sort_criteria.map do |attribute, order|
[convert_attribute(attribute), order]
represented.columns.map do |column|
::API::V3::Queries::Columns::QueryColumnRepresenter.new(column)
end
},
embedded: true,
if: ->(*) {
embed_links
}
property :group_by,
exec_context: :decorator,
getter: ->(*) {
represented.grouped? ? convert_attribute(represented.group_by) : nil
return unless represented.grouped?
column = represented.group_by_column
::API::V3::Queries::GroupBys::QueryGroupByRepresenter.new(column)
},
embedded: true,
if: ->(*) {
embed_links
},
render_nil: true
property :display_sums, getter: -> (*) { display_sums }
property :is_starred, getter: -> (*) { starred }
property :results,
exec_context: :decorator,
render_nil: true,
embedded: true,
if: ->(*) {
results
}
self.to_eager_load = [:query_menu_item,
project: { work_package_custom_fields: :translations }]
@ -84,6 +176,15 @@ module API
::API::Utilities::PropertyNameConverter.from_ar_name(attribute)
end
def map_with_sort_by_as_decorated
represented.sort_criteria.map do |attribute, order|
decorated = ::API::V3::Queries::SortBys::SortByDecorator.new(attribute,
order)
yield decorated
end
end
def _type
'Query'
end

@ -0,0 +1,73 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Queries
module SortBys
class QuerySortByRepresenter < ::API::Decorators::Single
self_link id_attribute: ->(*) { self_link_params },
title_getter: ->(*) { represented.name }
def initialize(model)
super(model, current_user: nil, embed_links: true)
end
link :column do
{
href: api_v3_paths.query_column(represented.converted_name),
title: represented.column_caption
}
end
link :direction do
{
href: represented.direction_uri,
title: represented.direction_l10n
}
end
property :id
property :name
private
def self_link_params
[represented.converted_name, represented.direction_name]
end
def _type
'QuerySortBy'
end
end
end
end
end
end

@ -0,0 +1,68 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Queries
module SortBys
class QuerySortBysAPI < ::API::OpenProjectAPI
resource :sort_bys do
helpers do
def convert_to_ar(attribute)
::API::Utilities::WpPropertyNameConverter.to_ar_name(attribute)
end
end
params do
requires :id, desc: 'Group by id'
requires :direction, desc: 'Direction of sorting'
end
before do
authorize(:view_work_packages, global: true, user: current_user)
end
namespace ':id-:direction' do
get do
ar_id = convert_to_ar(params[:id])
begin
decorator = ::API::V3::Queries::SortBys::SortByDecorator.new(ar_id,
params[:direction])
::API::V3::Queries::SortBys::QuerySortByRepresenter.new(decorator)
rescue ArgumentError
raise API::Errors::NotFound
end
end
end
end
end
end
end
end
end

@ -0,0 +1,99 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Queries
module SortBys
class SortByDecorator
def initialize(column_name, direction)
if !['asc', 'desc'].include?(direction)
raise ArgumentError, "Invalid direction. Only 'asc' and 'desc' are supported."
end
self.direction = direction
column_sym = column_name.to_sym
column = Query
.sortable_columns
.detect { |candidate| candidate.name == column_sym }
if column.nil?
raise ArgumentError, "Invalid column name."
end
self.column = column
end
def id
"#{converted_name}-#{direction_name}"
end
def name
I18n.t('query.attribute_and_direction',
attribute: column_caption,
direction: direction_l10n)
end
def converted_name
convert_attribute(column_name)
end
def direction_name
direction
end
def direction_uri
"urn:openproject-org:api:v3:queries:directions:#{direction}"
end
def direction_l10n
I18n.t(direction == 'desc' ? :label_descending : :label_ascending)
end
def column_name
column.name
end
def column_caption
column.caption
end
private
def convert_attribute(attribute)
::API::Utilities::PropertyNameConverter.from_ar_name(attribute)
end
attr_accessor :direction, :column
end
end
end
end
end

@ -143,6 +143,26 @@ module API
"#{query(id)}/unstar"
end
def self.query_column(name)
"#{queries}/columns/#{name}"
end
def self.query_group_by(name)
"#{queries}/group_bys/#{name}"
end
def self.query_sort_by(name, direction)
"#{queries}/sort_bys/#{name}-#{direction}"
end
def self.query_filter(name)
"#{queries}/filters/#{name}"
end
def self.query_operator(name)
"#{queries}/operators/#{name}"
end
def self.relation(id)
"#{root}/relations/#{id}"
end

@ -1,210 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module WorkPackages
module WorkPackageListHelpers
extend Grape::API::Helpers
include QueriesHelper
def work_packages_by_params(project: nil)
query = Query.new(name: '_', project: project)
query_params = {}
begin
apply_filters query, query_params
apply_sorting query, query_params
groups = apply_and_generate_groups query, query_params
total_sums = generate_total_sums query.results, query_params
rescue ::JSON::ParserError => error
raise ::API::Errors::InvalidQuery.new(error.message)
end
work_packages = query
.results
.sorted_work_packages
collection_representer(work_packages,
project: project,
query_params: query_params,
groups: groups,
sums: total_sums)
end
def apply_filters(query, query_params)
if params[:filters]
filters = parse_filters_from_json(params[:filters])
set_filters(query, filters)
query_params[:filters] = params[:filters]
end
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
operators[attribute] = filter[attribute]['operator']
values[attribute] = filter[attribute]['values']
end
{
fields: values.keys,
operators: operators,
values: values
}
end
def set_filters(query, filters)
add_filter_from_params(query, filters: filters)
bad_filter = query.filters.detect(&:invalid?)
if bad_filter
raise_invalid_query(bad_filter.errors)
end
end
def apply_sorting(query, query_params)
if params[:sortBy]
query.sort_criteria = parse_sorting_from_json(params[:sortBy])
query_params[:sortBy] = params[:sortBy]
else
query.sort_criteria = [['parent', 'desc']]
query_params[:sortBy] = 'parent:desc'
end
end
def parse_sorting_from_json(json)
JSON.parse(json).map do |(attribute, order)|
[convert_attribute(attribute), order]
end
end
def apply_and_generate_groups(query, query_params)
if params[:groupBy]
query.group_by = convert_attribute params[:groupBy]
query_params[:groupBy] = params[:groupBy]
generate_groups query.results
end
end
def generate_groups(results)
results.work_package_count_by_group.map do |group, count|
sums = nil
if params[:showSums] == 'true'
sums = format_query_sums results.all_sums_for_group(group)
end
::API::Decorators::AggregationGroup.new(group, count, sums: sums)
end
end
def generate_total_sums(results, query_params)
if params[:showSums] == 'true'
query_params[:showSums] = 'true'
format_query_sums results.all_total_sums
end
end
def format_query_sums(sums)
OpenStruct.new(format_column_keys(sums))
end
def format_column_keys(hash_by_column)
::Hash[
hash_by_column.map do |column, value|
match = /cf_(\d+)/.match(column.name.to_s)
column_name = if match
"custom_field_#{match[1]}"
else
column.name.to_s
end
[column_name, value]
end
]
end
def collection_representer(work_packages, project:, query_params:, groups:, sums:)
self_link = if project
api_v3_paths.work_packages_by_project(project.id)
else
api_v3_paths.work_packages
end
::API::V3::WorkPackages::WorkPackageCollectionRepresenter.new(
work_packages,
self_link,
project: project,
query: query_params,
page: to_i_or_nil(params[:offset]),
per_page: to_i_or_nil(params[:pageSize]),
groups: groups,
total_sums: sums,
embed_schemas: true,
current_user: current_user
)
end
def convert_attribute(attribute, append_id: false)
@@conversion_wp ||= ::API::Utilities::PropertyNameConverterQueryContext.new
::API::Utilities::PropertyNameConverter.to_ar_name(attribute,
context: @@conversion_wp,
refer_to_ids: append_id)
end
def raise_invalid_query(errors)
api_errors = errors.full_messages.map do |message|
::API::Errors::InvalidQuery.new(message)
end
raise ::API::Errors::MultipleErrors.create_if_many api_errors
end
def to_i_or_nil(value)
value ? value.to_i : nil
end
end
end
end
end

@ -34,7 +34,6 @@ module API
module WorkPackages
class WorkPackagesAPI < ::API::OpenProjectAPI
resources :work_packages do
helpers ::API::V3::WorkPackages::WorkPackageListHelpers
helpers ::API::V3::WorkPackages::CreateWorkPackages
# The enpoint needs to be mounted before the GET :work_packages/:id.
@ -45,7 +44,19 @@ module API
get do
authorize(:view_work_packages, global: true)
work_packages_by_params
service = WorkPackageCollectionFromQueryParamsService
.new(current_user)
.call(params)
if service.success?
service.result
else
api_errors = service.errors.full_messages.map do |message|
::API::Errors::InvalidQuery.new(message)
end
raise ::API::Errors::MultipleErrors.create_if_many api_errors
end
end
post do

@ -26,7 +26,6 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'api/v3/work_packages/work_package_list_helpers'
require 'api/v3/work_packages/create_work_packages'
module API
@ -35,11 +34,17 @@ module API
class WorkPackagesByProjectAPI < ::API::OpenProjectAPI
resources :work_packages do
helpers ::API::V3::WorkPackages::CreateWorkPackages
helpers ::API::V3::WorkPackages::WorkPackageListHelpers
get do
authorize(:view_work_packages, context: @project)
work_packages_by_params(project: @project)
service = raise_invalid_query_on_service_failure do
WorkPackageCollectionFromQueryParamsService
.new(current_user)
.call(params.merge(project: @project))
end
service.result
end
post do

@ -171,6 +171,15 @@ describe ::API::Utilities::PropertyNameConverter do
end
end
end
context 'inappropriate replacement as context does not respond to it with foreign key' do
let(:attribute_name) { 'type' }
subject { described_class.to_ar_name(attribute_name, context: context, refer_to_ids: true) }
it 'does not take the special replacement but appends the id suffix' do
is_expected.to eql('type_id')
end
end
end
end
end

@ -0,0 +1,90 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe ::API::V3::Queries::Columns::QueryColumnRepresenter do
include ::API::V3::Utilities::PathHelper
let(:column) { Query.available_columns.detect { |column| column.name == :status } }
let(:representer) { described_class.new(column) }
subject { representer.to_json }
describe 'generation' do
describe '_links' do
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.query_column 'status' }
let(:title) { 'Status' }
end
end
it 'has _type QueryColumn' do
is_expected
.to be_json_eql('QueryColumn'.to_json)
.at_path('_type')
end
it 'has id attribute' do
is_expected
.to be_json_eql('status'.to_json)
.at_path('id')
end
it 'has name attribute' do
is_expected
.to be_json_eql('Status'.to_json)
.at_path('name')
end
context 'for a translated column' do
let(:column) { Query.available_columns.detect { |column| column.name == :assigned_to } }
describe '_links' do
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.query_column 'assignee' }
let(:title) { 'Assignee' }
end
end
it 'has id attribute' do
is_expected
.to be_json_eql('assignee'.to_json)
.at_path('id')
end
it 'has name attribute' do
is_expected
.to be_json_eql('Assignee'.to_json)
.at_path('name')
end
end
end
end

@ -0,0 +1,111 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe ::API::V3::Queries::Filters::QueryFilterInstanceRepresenter do
include ::API::V3::Utilities::PathHelper
let(:operator) { '=' }
let(:values) { [status.id.to_s] }
let(:status) { FactoryGirl.build_stubbed(:status) }
let(:filter) do
Queries::WorkPackages::Filter::StatusFilter.new(operator: operator, values: values)
end
let(:representer) { described_class.new(filter) }
before do
allow(filter)
.to receive(:value_objects)
.and_return([status])
end
describe 'generation' do
subject { representer.to_json }
describe '_links' do
it_behaves_like 'has a titled link' do
let(:link) { 'filter' }
let(:href) { api_v3_paths.query_filter 'status' }
let(:title) { 'Status' }
end
it_behaves_like 'has a titled link' do
let(:link) { 'operator' }
let(:href) { api_v3_paths.query_operator '=' }
let(:title) { 'is' }
end
it "has a 'values' collection" do
expected = {
href: api_v3_paths.status(status.id.to_s),
title: status.name
}
is_expected
.to be_json_eql([expected].to_json)
.at_path('_links/values')
end
end
it 'has _type StatusQueryFilter' do
is_expected
.to be_json_eql('StatusQueryFilter'.to_json)
.at_path('_type')
end
it 'has name Status' do
is_expected
.to be_json_eql('Status'.to_json)
.at_path('name')
end
context 'with a non ar object filter' do
let(:values) { ['lorem ipsum'] }
let(:filter) do
Queries::WorkPackages::Filter::SubjectFilter.new(operator: operator, values: values)
end
describe '_links' do
it 'has no values link' do
is_expected
.not_to have_json_path('_links/values')
end
end
it "has a 'values' array property" do
is_expected
.to be_json_eql(values.to_json)
.at_path('values')
end
end
end
end

@ -0,0 +1,106 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe ::API::V3::Queries::Filters::QueryFilterRepresenter do
include ::API::V3::Utilities::PathHelper
let(:filter) { Queries::WorkPackages::Filter::SubjectFilter.new }
let(:representer) { described_class.new(filter) }
subject { representer.to_json }
describe 'generation' do
describe '_links' do
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.query_filter 'subject' }
let(:title) { 'Subject' }
end
end
it 'has _type QueryFilter' do
is_expected
.to be_json_eql('QueryFilter'.to_json)
.at_path('_type')
end
it 'has id attribute' do
is_expected
.to be_json_eql('subject'.to_json)
.at_path('id')
end
context 'for a translated filter' do
let(:filter) { Queries::WorkPackages::Filter::AssignedToFilter.new }
describe '_links' do
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.query_filter 'assignee' }
let(:title) { 'Assignee' }
end
end
it 'has id attribute' do
is_expected
.to be_json_eql('assignee'.to_json)
.at_path('id')
end
end
context 'for a custom field filter' do
let(:custom_field) { FactoryGirl.build_stubbed(:list_wp_custom_field) }
let(:filter) { Queries::WorkPackages::Filter::CustomFieldFilter.new }
before do
allow(WorkPackageCustomField)
.to receive(:find_by_id)
.with(custom_field.id)
.and_return custom_field
filter.name = "cf_#{custom_field.id}"
end
describe '_links' do
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.query_filter "customField#{custom_field.id}" }
let(:title) { custom_field.name }
end
end
it 'has id attribute' do
is_expected
.to be_json_eql("customField#{custom_field.id}".to_json)
.at_path('id')
end
end
end
end

@ -0,0 +1,90 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe ::API::V3::Queries::GroupBys::QueryGroupByRepresenter do
include ::API::V3::Utilities::PathHelper
let(:column) { Query.available_columns.detect { |column| column.name == :status } }
let(:representer) { described_class.new(column) }
subject { representer.to_json }
describe 'generation' do
describe '_links' do
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.query_group_by 'status' }
let(:title) { 'Status' }
end
end
it 'has _type QueryGroupBy' do
is_expected
.to be_json_eql('QueryGroupBy'.to_json)
.at_path('_type')
end
it 'has id attribute' do
is_expected
.to be_json_eql('status'.to_json)
.at_path('id')
end
it 'has name attribute' do
is_expected
.to be_json_eql('Status'.to_json)
.at_path('name')
end
context 'for a translated column' do
let(:column) { Query.available_columns.detect { |column| column.name == :assigned_to } }
describe '_links' do
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.query_group_by 'assignee' }
let(:title) { 'Assignee' }
end
end
it 'has id attribute' do
is_expected
.to be_json_eql('assignee'.to_json)
.at_path('id')
end
it 'has name attribute' do
is_expected
.to be_json_eql('Assignee'.to_json)
.at_path('name')
end
end
end
end

@ -0,0 +1,66 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe ::API::V3::Queries::Operators::QueryOperatorRepresenter do
include ::API::V3::Utilities::PathHelper
let(:operator) { '!~' }
let(:representer) { described_class.new(operator) }
subject { representer.to_json }
describe 'generation' do
describe '_links' do
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.query_operator operator }
let(:title) { I18n.t(:label_not_contains) }
end
end
it 'has _type QueryOperator' do
is_expected
.to be_json_eql('QueryOperator'.to_json)
.at_path('_type')
end
it 'has id attribute' do
is_expected
.to be_json_eql(operator.to_json)
.at_path('id')
end
it 'has name attribute' do
is_expected
.to be_json_eql(I18n.t(:label_not_contains).to_json)
.at_path('name')
end
end
end

@ -31,10 +31,11 @@ require 'spec_helper'
describe ::API::V3::Queries::QueryRepresenter do
include ::API::V3::Utilities::PathHelper
let(:query) {
FactoryGirl.build_stubbed(:query)
}
let(:representer) { described_class.new(query, current_user: double('current_user')) }
let(:query) { FactoryGirl.build_stubbed(:query, project: project) }
let(:project) { FactoryGirl.build_stubbed(:project) }
let(:representer) do
described_class.new(query, current_user: double('current_user'), embed_links: true)
end
subject { representer.to_json }
@ -58,12 +59,194 @@ describe ::API::V3::Queries::QueryRepresenter do
let(:title) { query.project.name }
end
it_behaves_like 'has an untitled link' do
let(:link) { 'results' }
let(:href) do
params = {
offset: 1,
pageSize: Setting.per_page_options_array.first
}
"#{api_v3_paths.work_packages_by_project(project.id)}?#{params.to_query}"
end
end
context 'has no project' do
let(:query) { FactoryGirl.build_stubbed(:query, project: nil) }
it_behaves_like 'has an empty link' do
let(:link) { 'project' }
end
it_behaves_like 'has an untitled link' do
let(:link) { 'results' }
let(:href) do
params = {
offset: 1,
pageSize: Setting.per_page_options_array.first
}
"#{api_v3_paths.work_packages}?#{params.to_query}"
end
end
end
context 'with filter, sort, group by and pageSize' do
let(:representer) do
described_class.new(query,
current_user: double('current_user'))
end
let(:query) do
query = FactoryGirl.build_stubbed(:query, project: project)
query.add_filter('subject', '~', ['bogus'])
query.group_by = 'author'
query.sort_criteria = [['assigned_to', 'asc'], ['type', 'desc']]
query
end
let(:expected_href) do
params = {
offset: 1,
pageSize: Setting.per_page_options_array.first,
filters: JSON::dump([{ subject: { operator: '~', values: ['bogus'] } }]),
groupBy: 'author',
sortBy: JSON::dump([['assignee', 'asc'], ['type', 'desc']])
}
api_v3_paths.work_packages_by_project(project.id) + "?#{params.to_query}"
end
it_behaves_like 'has an untitled link' do
let(:link) { 'results' }
let(:href) { expected_href }
end
end
context 'with offset and page size' do
let(:representer) do
described_class.new(query,
current_user: double('current_user'),
params: { offset: 2, pageSize: 25 })
end
let(:expected_href) do
params = {
offset: 2,
pageSize: 25
}
api_v3_paths.work_packages_by_project(project.id) + "?#{params.to_query}"
end
it_behaves_like 'has an untitled link' do
let(:link) { 'results' }
let(:href) { expected_href }
end
end
context 'without columns' do
let(:query) do
query = FactoryGirl.build_stubbed(:query, project: project)
# need to write bogus here because the query
# will otherwise sport the default columns
query.column_names = ['blubs']
query
end
it 'has an empty columns array' do
is_expected
.to be_json_eql([].to_json)
.at_path('_links/columns')
end
end
context 'with columns' do
let(:query) do
query = FactoryGirl.build_stubbed(:query, project: project)
query.column_names = ['status', 'assigned_to', 'updated_at']
query
end
it 'has an array of columns' do
status = {
href: '/api/v3/queries/columns/status',
title: 'Status'
}
assignee = {
href: '/api/v3/queries/columns/assignee',
title: 'Assignee'
}
subproject = {
href: '/api/v3/queries/columns/updated_at',
title: 'Updated on'
}
expected = [status, assignee, subproject]
is_expected
.to be_json_eql(expected.to_json)
.at_path('_links/columns')
end
end
context 'without group_by' do
it_behaves_like 'has a titled link' do
let(:href) { nil }
let(:link) { 'groupBy' }
let(:title) { nil }
end
end
context 'with group_by' do
let(:query) do
query = FactoryGirl.build_stubbed(:query, project: project)
query.group_by = 'status'
query
end
it_behaves_like 'has a titled link' do
let(:href) { '/api/v3/queries/group_bys/status' }
let(:link) { 'groupBy' }
let(:title) { 'Status' }
end
end
context 'without sort_by' do
it 'has an empty sortBy array' do
is_expected
.to be_json_eql([].to_json)
.at_path('_links/sortBy')
end
end
context 'with sort_by' do
let(:query) do
FactoryGirl.build_stubbed(:query,
sort_criteria: [['subject', 'asc'], ['assigned_to', 'desc']])
end
it 'has an array of sortBy' do
expected = [
{
href: api_v3_paths.query_sort_by('subject', 'asc'),
title: 'Subject (Ascending)'
},
{
href: api_v3_paths.query_sort_by('assignee', 'desc'),
title: 'Assignee (Descending)'
}
]
is_expected
.to be_json_eql(expected.to_json)
.at_path('_links/sortBy')
end
end
end
@ -76,68 +259,178 @@ describe ::API::V3::Queries::QueryRepresenter do
end
it 'should indicate whether sums are shown' do
is_expected.to be_json_eql(query.display_sums.to_json).at_path('displaySums')
is_expected.to be_json_eql(query.display_sums.to_json).at_path('sums')
end
it 'should indicate whether the query is publicly visible' do
is_expected.to be_json_eql(query.is_public.to_json).at_path('isPublic')
end
describe 'grouping' do
let(:query) { FactoryGirl.build_stubbed(:query, group_by: 'assigned_to') }
it 'should show the grouping column' do
is_expected.to be_json_eql('assignee'.to_json).at_path('groupBy')
end
context 'without grouping' do
let(:query) { FactoryGirl.build_stubbed(:query, group_by: nil) }
it 'should show no grouping column' do
is_expected.to be_json_eql(nil.to_json).at_path('groupBy')
end
end
is_expected.to be_json_eql(query.is_public.to_json).at_path('public')
end
describe 'with filters' do
let(:query) do
query = FactoryGirl.build_stubbed(:query)
query.add_filter('status_id', '=', ['1'])
query.add_filter('status_id', '=', [filter_status.id.to_s])
allow(query.filters.last)
.to receive(:value_objects)
.and_return([filter_status])
query.add_filter('assigned_to_id', '!', [filter_user.id.to_s])
allow(query.filters.last)
.to receive(:value_objects)
.and_return([filter_user])
query
end
let(:filter_status) { FactoryGirl.build_stubbed(:status) }
let(:filter_user) { FactoryGirl.build_stubbed(:user) }
it 'should render the filters' do
expected = [
{
status: {
operator: '=',
values: ['1']
}
expected_status = {
"_type": "StatusQueryFilter",
"name": "Status",
"_links": {
"filter": {
"href": "/api/v3/queries/filters/status",
"title": "Status"
},
"operator": {
"href": "/api/v3/queries/operators/=",
"title": "is"
},
"values": [
{
"href": api_v3_paths.status(filter_status.id),
"title": filter_status.name
}
]
}
]
}
expected_assignee = {
"_type": "AssigneeQueryFilter",
"name": "Assignee",
"_links": {
"filter": {
"href": "/api/v3/queries/filters/assignee",
"title": "Assignee"
},
"operator": {
"href": "/api/v3/queries/operators/!",
"title": "is not"
},
"values": [
{
"href": api_v3_paths.user(filter_user.id),
"title": filter_user.name
}
]
}
}
expected = [expected_status, expected_assignee]
is_expected.to be_json_eql(expected.to_json).at_path('filters')
end
end
describe 'with sort criteria' do
let(:query) {
let(:query) do
FactoryGirl.build_stubbed(:query,
sort_criteria: [['subject', 'asc'], ['assigned_to', 'desc']])
}
end
it 'should render the filters' do
is_expected.to be_json_eql([
['subject', 'asc'],
['assignee', 'desc']
].to_json).at_path('sortCriteria')
it 'has the sort criteria embedded' do
is_expected
.to be_json_eql('/api/v3/queries/sort_bys/subject-asc'.to_json)
.at_path('_embedded/sortBy/0/_links/self/href')
is_expected
.to be_json_eql('/api/v3/queries/sort_bys/assignee-desc'.to_json)
.at_path('_embedded/sortBy/1/_links/self/href')
end
end
describe 'with columns' do
let(:query) { FactoryGirl.build_stubbed(:query, column_names: ['subject', 'assigned_to']) }
let(:query) do
query = FactoryGirl.build_stubbed(:query, project: project)
it 'should render the filters' do
is_expected.to be_json_eql(['subject', 'assignee'].to_json).at_path('columnNames')
query.column_names = ['status', 'assigned_to', 'updated_at']
query
end
it 'has the columns embedded' do
is_expected
.to be_json_eql('/api/v3/queries/columns/status'.to_json)
.at_path('_embedded/columns/0/_links/self/href')
end
context 'when not embedding' do
let(:representer) do
described_class.new(query, current_user: double('current_user'), embed_links: false)
end
it 'has no columns embedded' do
is_expected
.not_to have_json_path('_embedded/columns')
end
end
end
describe 'with group by' do
let(:query) do
query = FactoryGirl.build_stubbed(:query, project: project)
query.group_by = 'status'
query
end
it 'has the group by embedded' do
is_expected
.to be_json_eql('/api/v3/queries/group_bys/status'.to_json)
.at_path('_embedded/groupBy/_links/self/href')
end
context 'when not embedding' do
let(:representer) do
described_class.new(query, current_user: double('current_user'), embed_links: false)
end
it 'has no group bys embedded' do
is_expected
.not_to have_json_path('_embedded/groupBy')
end
end
end
describe 'embedded results' do
let(:query) { FactoryGirl.build_stubbed(:query) }
let(:representer) do
described_class.new(query,
current_user: double('current_user'),
results: results_representer)
end
context 'results are provided' do
let(:results_representer) do
{
_type: 'BogusResultType'
}
end
it 'should embed the results' do
is_expected
.to be_json_eql('BogusResultType'.to_json)
.at_path('_embedded/results/_type')
end
end
context 'no results provided' do
let(:results_representer) { nil }
it 'should not embed the results' do
is_expected
.not_to have_json_path('_embedded/results')
end
end
end
end

@ -0,0 +1,130 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe ::API::V3::Queries::SortBys::QuerySortByRepresenter do
include ::API::V3::Utilities::PathHelper
let(:column) { 'status' }
let(:direction) { 'desc' }
let(:representer) do
described_class
.new(::API::V3::Queries::SortBys::SortByDecorator.new(column, direction))
end
subject { representer.to_json }
describe 'generation' do
describe '_links' do
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.query_sort_by 'status', 'desc' }
let(:title) { 'Status (Descending)' }
end
end
it 'has _type QuerySortBy' do
is_expected
.to be_json_eql('QuerySortBy'.to_json)
.at_path('_type')
end
it 'has id attribute' do
is_expected
.to be_json_eql('status-desc'.to_json)
.at_path('id')
end
it 'has name attribute' do
is_expected
.to be_json_eql('Status (Descending)'.to_json)
.at_path('name')
end
it_behaves_like 'has a titled link' do
let(:link) { 'column' }
let(:href) { api_v3_paths.query_column 'status' }
let(:title) { 'Status' }
end
it_behaves_like 'has a titled link' do
let(:link) { 'direction' }
let(:href) { "urn:openproject-org:api:v3:queries:directions:#{direction}" }
let(:title) { 'Descending' }
end
context 'when providing an unsupported sort direction' do
let(:direction) { 'bogus' }
it 'raises error' do
expect { subject }.to raise_error(ArgumentError)
end
end
context 'when sorting differently' do
let(:direction) { 'asc' }
it 'has id attribute' do
is_expected
.to be_json_eql('status-asc'.to_json)
.at_path('id')
end
it 'has name attribute' do
is_expected
.to be_json_eql('Status (Ascending)'.to_json)
.at_path('name')
end
end
context 'for a translated column' do
let(:column) { 'assigned_to' }
describe '_links' do
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.query_sort_by 'assignee', 'desc' }
let(:title) { 'Assignee (Descending)' }
end
end
it 'has id attribute' do
is_expected
.to be_json_eql('assignee-desc'.to_json)
.at_path('id')
end
it 'has name attribute' do
is_expected
.to be_json_eql('Assignee (Descending)'.to_json)
.at_path('name')
end
end
end
end

@ -242,6 +242,36 @@ describe ::API::V3::Utilities::PathHelper do
it_behaves_like 'api v3 path', '/queries/1/unstar'
end
describe '#query_column' do
subject { helper.query_column 'updated_on' }
it_behaves_like 'api v3 path', '/queries/columns/updated_on'
end
describe '#query_group_by' do
subject { helper.query_group_by 'status' }
it_behaves_like 'api v3 path', '/queries/group_bys/status'
end
describe '#query_sort_by' do
subject { helper.query_sort_by 'status', 'desc' }
it_behaves_like 'api v3 path', '/queries/sort_bys/status-desc'
end
describe '#query_filter' do
subject { helper.query_filter 'status' }
it_behaves_like 'api v3 path', '/queries/filters/status'
end
describe '#query_operator' do
subject { helper.query_operator '=' }
it_behaves_like 'api v3 path', '/queries/operators/='
end
describe 'relations paths' do
describe '#relation' do
subject { helper.relation 1 }

@ -45,9 +45,9 @@ describe Queries::AvailableFilters, type: :model do
let(:includer) do
includer = HelperClass.new(context)
allow(includer)
.to receive(:filter_register)
.and_return(registered_filters)
allow(Queries::Register)
.to receive(:filters)
.and_return(HelperClass => registered_filters)
includer
end

@ -78,5 +78,30 @@ describe Queries::WorkPackages::Filter::CategoryFilter, type: :model do
.to match_array [[category.name, category.id.to_s]]
end
end
describe '#value_objects' do
let(:category1) { FactoryGirl.build_stubbed(:category) }
let(:category2) { FactoryGirl.build_stubbed(:category) }
before do
allow(project)
.to receive(:categories)
.and_return [category1, category2]
instance.values = [category2.id.to_s]
end
it 'returns an array of category' do
expect(instance.value_objects)
.to match_array [category2]
end
end
describe '#ar_object_filter?' do
it 'is true' do
expect(instance)
.to be_ar_object_filter
end
end
end
end

@ -45,5 +45,7 @@ describe Queries::WorkPackages::Filter::CreatedAtFilter, type: :model do
expect(instance.allowed_values).to be_nil
end
end
it_behaves_like 'non ar filter'
end
end

@ -393,4 +393,108 @@ describe Queries::WorkPackages::Filter::CustomFieldFilter, type: :model do
end
end
end
describe '#ar_object_filter? / #value_objects' do
context 'list cf' do
let(:custom_field) { list_wp_custom_field }
it_behaves_like 'non ar filter'
end
context 'bool cf' do
let(:custom_field) { bool_wp_custom_field }
it_behaves_like 'non ar filter'
end
context 'int cf' do
let(:custom_field) { int_wp_custom_field }
it_behaves_like 'non ar filter'
end
context 'float cf' do
let(:custom_field) { float_wp_custom_field }
it_behaves_like 'non ar filter'
end
context 'text cf' do
let(:custom_field) { text_wp_custom_field }
it_behaves_like 'non ar filter'
end
context 'user cf' do
let(:custom_field) { user_wp_custom_field }
describe '#ar_object_filter?' do
it 'is true' do
expect(instance)
.to be_ar_object_filter
end
end
describe '#value_objects' do
let(:user1) { FactoryGirl.build_stubbed(:user) }
let(:user2) { FactoryGirl.build_stubbed(:user) }
before do
allow(User)
.to receive(:find)
.with([user1.id.to_s, user2.id.to_s])
.and_return([user1, user2])
instance.values = [user1.id.to_s, user2.id.to_s]
end
it 'returns an array with users' do
expect(instance.value_objects)
.to match_array([user1, user2])
end
end
end
context 'version cf' do
let(:custom_field) { version_wp_custom_field }
describe '#ar_object_filter?' do
it 'is true' do
expect(instance)
.to be_ar_object_filter
end
end
describe '#value_objects' do
let(:version1) { FactoryGirl.build_stubbed(:version) }
let(:version2) { FactoryGirl.build_stubbed(:version) }
before do
allow(Version)
.to receive(:find)
.with([version1.id.to_s, version2.id.to_s])
.and_return([version1, version2])
instance.values = [version1.id.to_s, version2.id.to_s]
end
it 'returns an array with users' do
expect(instance.value_objects)
.to match_array([version1, version2])
end
end
end
context 'date cf' do
let(:custom_field) { date_wp_custom_field }
it_behaves_like 'non ar filter'
end
context 'string cf' do
let(:custom_field) { string_wp_custom_field }
it_behaves_like 'non ar filter'
end
end
end

@ -45,5 +45,7 @@ describe Queries::WorkPackages::Filter::DueDateFilter, type: :model do
expect(instance.allowed_values).to be_nil
end
end
it_behaves_like 'non ar filter'
end
end

@ -45,5 +45,7 @@ describe Queries::WorkPackages::Filter::EstimatedHoursFilter, type: :model do
expect(instance.allowed_values).to be_nil
end
end
it_behaves_like 'non ar filter'
end
end

@ -67,5 +67,29 @@ describe Queries::WorkPackages::Filter::GroupFilter, type: :model do
.to match_array [[group.name, group.id.to_s]]
end
end
describe '#ar_object_filter?' do
it 'is true' do
expect(instance)
.to be_ar_object_filter
end
end
describe '#value_objects' do
let(:group2) { FactoryGirl.build_stubbed(:group) }
before do
allow(Group)
.to receive(:all)
.and_return([group, group2])
instance.values = [group2.id.to_s]
end
it 'returns an array of groups' do
expect(instance.value_objects)
.to match_array([group2])
end
end
end
end

@ -66,5 +66,29 @@ describe Queries::WorkPackages::Filter::PriorityFilter, type: :model do
.to match_array [[priority.name, priority.id.to_s]]
end
end
describe '#ar_object_filter?' do
it 'is true' do
expect(instance)
.to be_ar_object_filter
end
end
describe '#value_objects' do
let(:priority2) { FactoryGirl.build_stubbed(:priority) }
before do
allow(IssuePriority)
.to receive(:active)
.and_return([priority, priority2])
instance.values = [priority2.id.to_s]
end
it 'returns an array of priorities' do
expect(instance.value_objects)
.to match_array([priority2])
end
end
end
end

@ -86,5 +86,30 @@ describe Queries::WorkPackages::Filter::ProjectFilter, type: :model do
["-- #{child.name}", child.id.to_s]]
end
end
describe '#ar_object_filter?' do
it 'is true' do
expect(instance)
.to be_ar_object_filter
end
end
describe '#value_objects' do
let(:project) { FactoryGirl.build_stubbed(:project) }
let(:project2) { FactoryGirl.build_stubbed(:project) }
before do
allow(Project)
.to receive(:visible)
.and_return([project, project2])
instance.values = [project.id.to_s]
end
it 'returns an array of projects' do
expect(instance.value_objects)
.to match_array([project])
end
end
end
end

@ -132,5 +132,31 @@ describe Queries::WorkPackages::Filter::ResponsibleFilter, type: :model do
end
end
end
describe '#ar_object_filter?' do
it 'is true' do
expect(instance)
.to be_ar_object_filter
end
end
describe '#value_objects' do
let(:user) { FactoryGirl.build_stubbed(:user) }
let(:user2) { FactoryGirl.build_stubbed(:user) }
before do
allow(Principal)
.to receive(:find)
.with([user.id.to_s, user2.id.to_s])
.and_return([user, user2])
instance.values = [user.id.to_s, user2.id.to_s]
end
it 'returns an array of projects' do
expect(instance.value_objects)
.to match_array([user, user2])
end
end
end
end

@ -67,5 +67,29 @@ describe Queries::WorkPackages::Filter::RoleFilter, type: :model do
.to match_array [[role.name, role.id.to_s]]
end
end
describe '#ar_object_filter?' do
it 'is true' do
expect(instance)
.to be_ar_object_filter
end
end
describe '#value_objects' do
let(:role2) { FactoryGirl.build_stubbed(:role) }
before do
allow(Role)
.to receive(:givable)
.and_return([role, role2])
instance.values = [role.id.to_s, role2.id.to_s]
end
it 'returns an array of projects' do
expect(instance.value_objects)
.to match_array([role, role2])
end
end
end
end

@ -45,5 +45,7 @@ describe Queries::WorkPackages::Filter::StartDateFilter, type: :model do
expect(instance.allowed_values).to be_nil
end
end
it_behaves_like 'non ar filter'
end
end

@ -30,6 +30,7 @@ require 'spec_helper'
describe Queries::WorkPackages::Filter::StatusFilter, type: :model do
let(:status) { FactoryGirl.build_stubbed(:status) }
let(:status2) { FactoryGirl.build_stubbed(:status) }
it_behaves_like 'basic query filter' do
let(:order) { 1 }
@ -66,5 +67,27 @@ describe Queries::WorkPackages::Filter::StatusFilter, type: :model do
.to match_array [[status.name, status.id.to_s]]
end
end
describe '#value_objects' do
before do
allow(Status)
.to receive(:all)
.and_return [status, status2]
end
it 'is an array of statuses' do
instance.values = [status.id.to_s]
expect(instance.value_objects)
.to match_array [status]
end
end
describe '#ar_object_filter?' do
it 'is true' do
expect(instance)
.to be_ar_object_filter
end
end
end
end

@ -28,7 +28,6 @@
require 'spec_helper'
describe Queries::WorkPackages::Filter::SubjectFilter, type: :model do
it_behaves_like 'basic query filter' do
let(:order) { 8 }
@ -46,5 +45,7 @@ describe Queries::WorkPackages::Filter::SubjectFilter, type: :model do
expect(instance.allowed_values).to be_nil
end
end
it_behaves_like 'non ar filter'
end
end

@ -101,7 +101,32 @@ describe Queries::WorkPackages::Filter::SubprojectFilter, type: :model do
it 'returns a list of all visible descendants' do
expect(instance.allowed_values).to match_array [[subproject1.name, subproject1.id.to_s],
[subproject2.name, subproject2.id.to_s]]
[subproject2.name, subproject2.id.to_s]]
end
end
describe '#ar_object_filter?' do
it 'is true' do
expect(instance)
.to be_ar_object_filter
end
end
describe '#value_objects' do
let(:subproject1) { FactoryGirl.build_stubbed(:project) }
let(:subproject2) { FactoryGirl.build_stubbed(:project) }
before do
allow(project)
.to receive_message_chain(:descendants, :visible)
.and_return([subproject1, subproject2])
instance.values = [subproject1.id.to_s, subproject2.id.to_s]
end
it 'returns an array of projects' do
expect(instance.value_objects)
.to match_array([subproject1, subproject2])
end
end
end

@ -108,5 +108,30 @@ describe Queries::WorkPackages::Filter::TypeFilter, type: :model do
end
end
end
describe '#ar_object_filter?' do
it 'is true' do
expect(instance)
.to be_ar_object_filter
end
end
describe '#value_objects' do
let(:type1) { FactoryGirl.build_stubbed(:type) }
let(:type2) { FactoryGirl.build_stubbed(:type) }
before do
allow(project)
.to receive(:rolled_up_types)
.and_return([type1, type2])
instance.values = [type1.id.to_s, type2.id.to_s]
end
it 'returns an array of types' do
expect(instance.value_objects)
.to match_array([type1, type2])
end
end
end
end

@ -45,5 +45,7 @@ describe Queries::WorkPackages::Filter::UpdatedAtFilter, type: :model do
expect(instance.allowed_values).to be_nil
end
end
it_behaves_like 'non ar filter'
end
end

@ -99,5 +99,30 @@ describe Queries::WorkPackages::Filter::VersionFilter, type: :model do
end
end
end
describe '#ar_object_filter?' do
it 'is true' do
expect(instance)
.to be_ar_object_filter
end
end
describe '#value_objects' do
let(:version1) { FactoryGirl.build_stubbed(:version) }
let(:version2) { FactoryGirl.build_stubbed(:version) }
before do
allow(project)
.to receive(:shared_versions)
.and_return([version1, version2])
instance.values = [version1.id.to_s]
end
it 'returns an array of versions' do
expect(instance.value_objects)
.to match_array([version1])
end
end
end
end

@ -142,5 +142,30 @@ describe Queries::WorkPackages::Filter::WatcherFilter, type: :model do
end
end
end
describe '#ar_object_filter?' do
it 'is true' do
expect(instance)
.to be_ar_object_filter
end
end
describe '#value_objects' do
let(:user1) { FactoryGirl.build_stubbed(:user) }
before do
allow(Principal)
.to receive(:find)
.with([user1.id.to_s])
.and_return([user1])
instance.values = [user1.id.to_s]
end
it 'returns an array of users' do
expect(instance.value_objects)
.to match_array([user1])
end
end
end
end

@ -0,0 +1,82 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'rack/test'
describe 'API v3 Query Column resource', type: :request do
include Rack::Test::Methods
include API::V3::Utilities::PathHelper
describe '#get queries/columns/:id' do
let(:path) { api_v3_paths.query_column(column_name) }
let(:column_name) { 'status' }
let(:project) { FactoryGirl.create(:project) }
let(:role) { FactoryGirl.create(:role, permissions: permissions) }
let(:permissions) { [:view_work_packages] }
let(:user) do
FactoryGirl.create(:user,
member_in_project: project,
member_through_role: role)
end
before do
allow(User)
.to receive(:current)
.and_return(user)
get path
end
it 'succeeds' do
expect(last_response.status)
.to eq(200)
end
it 'returns the column' do
expect(last_response.body)
.to be_json_eql(path.to_json)
.at_path('_links/self/href')
end
context 'user not allowed' do
let(:permissions) { [] }
it_behaves_like 'unauthorized access'
end
context 'non existing group by' do
let(:path) { api_v3_paths.query_column('bogus') }
it 'returns 404' do
expect(last_response.status)
.to eql(404)
end
end
end
end

@ -0,0 +1,98 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'rack/test'
describe 'API v3 Query Filter resource', type: :request do
include Rack::Test::Methods
include API::V3::Utilities::PathHelper
describe '#get queries/filters/:id' do
let(:path) { api_v3_paths.query_filter(filter_name) }
let(:filter_name) { 'assignee' }
let(:project) { FactoryGirl.create(:project) }
let(:role) { FactoryGirl.create(:role, permissions: permissions) }
let(:permissions) { [:view_work_packages] }
let(:user) do
FactoryGirl.create(:user,
member_in_project: project,
member_through_role: role)
end
before do
allow(User)
.to receive(:current)
.and_return(user)
get path
end
it 'succeeds' do
expect(last_response.status)
.to eq(200)
end
it 'returns the filter' do
expect(last_response.body)
.to be_json_eql(path.to_json)
.at_path('_links/self/href')
end
context 'user not allowed' do
let(:permissions) { [] }
it_behaves_like 'unauthorized access'
end
context 'non existing filter' do
let(:filter_name) { 'bogus' }
it 'returns 404' do
expect(last_response.status)
.to eql(404)
end
end
context 'custom field filter' do
let(:list_wp_custom_field) { FactoryGirl.create(:list_wp_custom_field) }
let(:filter_name) { "customField#{list_wp_custom_field.id}" }
it 'succeeds' do
expect(last_response.status)
.to eq(200)
end
it 'returns the filter' do
expect(last_response.body)
.to be_json_eql(path.to_json)
.at_path('_links/self/href')
end
end
end
end

@ -0,0 +1,91 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'rack/test'
describe 'API v3 Query Group By resource', type: :request do
include Rack::Test::Methods
include API::V3::Utilities::PathHelper
describe '#get queries/group_bys/:id' do
let(:path) { api_v3_paths.query_group_by(group_by_name) }
let(:group_by_name) { 'status' }
let(:project) { FactoryGirl.create(:project) }
let(:role) { FactoryGirl.create(:role, permissions: permissions) }
let(:permissions) { [:view_work_packages] }
let(:user) do
FactoryGirl.create(:user,
member_in_project: project,
member_through_role: role)
end
before do
allow(User)
.to receive(:current)
.and_return(user)
get path
end
it 'succeeds' do
expect(last_response.status)
.to eql(200)
end
it 'returns the group_by' do
expect(last_response.body)
.to be_json_eql(path.to_json)
.at_path('_links/self/href')
end
context 'user not allowed' do
let(:permissions) { [] }
it_behaves_like 'unauthorized access'
end
context 'non existing group by' do
let(:path) { api_v3_paths.query_group_by('bogus') }
it 'returns 404' do
expect(last_response.status)
.to eql(404)
end
end
context 'non groupable group by' do
let(:path) { api_v3_paths.query_group_by('id') }
it 'returns 404' do
expect(last_response.status)
.to eql(404)
end
end
end
end

@ -0,0 +1,82 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'rack/test'
describe 'API v3 Query Operator resource', type: :request do
include Rack::Test::Methods
include API::V3::Utilities::PathHelper
describe '#get queries/operators/:id' do
let(:path) { api_v3_paths.query_operator(operator) }
let(:operator) { '=' }
let(:project) { FactoryGirl.create(:project) }
let(:role) { FactoryGirl.create(:role, permissions: permissions) }
let(:permissions) { [:view_work_packages] }
let(:user) do
FactoryGirl.create(:user,
member_in_project: project,
member_through_role: role)
end
before do
allow(User)
.to receive(:current)
.and_return(user)
get path
end
it 'succeeds' do
expect(last_response.status)
.to eq(200)
end
it 'returns the operator' do
expect(last_response.body)
.to be_json_eql(path.to_json)
.at_path('_links/self/href')
end
context 'user not allowed' do
let(:permissions) { [] }
it_behaves_like 'unauthorized access'
end
context 'non existing operator' do
let(:operator) { 'bogus' }
it 'returns 404' do
expect(last_response.status)
.to eql(404)
end
end
end
end

@ -46,6 +46,7 @@ describe 'API v3 Query resource', type: :request do
let(:query) { FactoryGirl.create(:public_query, project: project) }
let(:other_query) { FactoryGirl.create(:public_query, project: other_project) }
let(:global_query) { FactoryGirl.create(:global_query) }
let(:work_package) { FactoryGirl.create(:work_package, project: project) }
before do
allow(User).to receive(:current).and_return current_user
@ -155,12 +156,22 @@ describe 'API v3 Query resource', type: :request do
describe '#get queries/:id' do
before do
work_package
get api_v3_paths.query(query.id)
end
it 'should succeed' do
expect(last_response.status).to eq(200)
end
it 'embedds the query results' do
expect(last_response.body)
.to be_json_eql('WorkPackageCollection'.to_json)
.at_path('_embedded/results/_type')
expect(last_response.body)
.to be_json_eql(api_v3_paths.work_package(work_package.id).to_json)
.at_path('_embedded/results/_embedded/elements/0/_links/self/href')
end
end
describe '#delete queries/:id' do
@ -216,8 +227,8 @@ describe 'API v3 Query resource', type: :request do
expect(last_response.status).to eq(200)
end
it 'should return the query with "isStarred" property set to true' do
expect(last_response.body).to be_json_eql(true).at_path('isStarred')
it 'should return the query with "starred" property set to true' do
expect(last_response.body).to be_json_eql(true).at_path('starred')
end
end
@ -226,8 +237,8 @@ describe 'API v3 Query resource', type: :request do
expect(last_response.status).to eq(200)
end
it 'should return the query with "isStarred" property set to true' do
expect(last_response.body).to be_json_eql(true).at_path('isStarred')
it 'should return the query with "starred" property set to true' do
expect(last_response.body).to be_json_eql(true).at_path('starred')
end
end
@ -258,8 +269,8 @@ describe 'API v3 Query resource', type: :request do
expect(last_response.status).to eq(200)
end
it 'should return the query with "isStarred" property set to true' do
expect(last_response.body).to be_json_eql(true).at_path('isStarred')
it 'should return the query with "starred" property set to true' do
expect(last_response.body).to be_json_eql(true).at_path('starred')
end
end
@ -299,8 +310,8 @@ describe 'API v3 Query resource', type: :request do
expect(last_response.status).to eq(200)
end
it 'should return the query with "isStarred" property set to false' do
expect(last_response.body).to be_json_eql(false).at_path('isStarred')
it 'should return the query with "starred" property set to false' do
expect(last_response.body).to be_json_eql(false).at_path('starred')
end
end
@ -313,8 +324,8 @@ describe 'API v3 Query resource', type: :request do
expect(last_response.status).to eq(200)
end
it 'should return the query with "isStarred" property set to true' do
expect(last_response.body).to be_json_eql(false).at_path('isStarred')
it 'should return the query with "starred" property set to true' do
expect(last_response.body).to be_json_eql(false).at_path('starred')
end
end
@ -354,8 +365,8 @@ describe 'API v3 Query resource', type: :request do
expect(last_response.status).to eq(200)
end
it 'should return the query with "isStarred" property set to true' do
expect(last_response.body).to be_json_eql(false).at_path('isStarred')
it 'should return the query with "starred" property set to true' do
expect(last_response.body).to be_json_eql(false).at_path('starred')
end
end

@ -0,0 +1,101 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'rack/test'
describe 'API v3 Query Sort Bys resource', type: :request do
include Rack::Test::Methods
include API::V3::Utilities::PathHelper
describe '#get queries/sort_bys/:id' do
let(:path) { api_v3_paths.query_sort_by(column_name, direction) }
let(:column_name) { 'status' }
let(:direction) { 'desc' }
let(:project) { FactoryGirl.create(:project) }
let(:role) { FactoryGirl.create(:role, permissions: permissions) }
let(:permissions) { [:view_work_packages] }
let(:user) do
FactoryGirl.create(:user,
member_in_project: project,
member_through_role: role)
end
before do
allow(User)
.to receive(:current)
.and_return(user)
get path
end
it 'succeeds' do
expect(last_response.status)
.to eq(200)
end
it 'returns the sort by' do
expect(last_response.body)
.to be_json_eql(path.to_json)
.at_path('_links/self/href')
end
context 'user not allowed' do
let(:permissions) { [] }
it_behaves_like 'unauthorized access'
end
context 'non existing sort by' do
let(:path) { api_v3_paths.query_sort_by('bogus', direction) }
it 'returns 404' do
expect(last_response.status)
.to eql(404)
end
end
context 'non existing direction' do
let(:path) { api_v3_paths.query_sort_by(column_name, 'bogus') }
it 'returns 404' do
expect(last_response.status)
.to eql(404)
end
end
context 'non sortable sort by' do
let(:path) { api_v3_paths.query_sort_by('spent_time', direction) }
it 'returns 404' do
expect(last_response.status)
.to eql(404)
end
end
end
end

@ -0,0 +1,231 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe ::API::V3::ParseQueryParamsService,
type: :model do
let(:instance) { described_class.new }
let(:params) { {} }
describe '#call' do
subject { instance.call(params) }
shared_examples_for 'transforms' do
it 'is success' do
expect(subject)
.to be_success
end
it 'is transformed' do
expect(subject.result)
.to eql(expected)
end
end
context 'with group by' do
context 'as groupBy' do
it_behaves_like 'transforms' do
let(:params) { { groupBy: 'status' } }
let(:expected) { { group_by: 'status' } }
end
end
context 'as group_by' do
it_behaves_like 'transforms' do
let(:params) { { group_by: 'status' } }
let(:expected) { { group_by: 'status' } }
end
end
context 'as "g"' do
it_behaves_like 'transforms' do
let(:params) { { g: 'status' } }
let(:expected) { { group_by: 'status' } }
end
end
context 'with an attribute called differently in v3' do
it_behaves_like 'transforms' do
let(:params) { { groupBy: 'assignee' } }
let(:expected) { { group_by: 'assigned_to' } }
end
end
end
context 'with columns' do
context 'as columns' do
it_behaves_like 'transforms' do
let(:params) { { columns: ['status', 'assignee'] } }
let(:expected) { { columns: ['status', 'assigned_to'] } }
end
end
context 'as "c"' do
it_behaves_like 'transforms' do
let(:params) { { c: ['status', 'assignee'] } }
let(:expected) { { columns: ['status', 'assigned_to'] } }
end
end
context 'as column_names' do
it_behaves_like 'transforms' do
let(:params) { { column_names: ['status', 'assignee'] } }
let(:expected) { { columns: ['status', 'assigned_to'] } }
end
end
end
context 'with sort' do
context 'as sortBy in comma separated value' do
it_behaves_like 'transforms' do
let(:params) { { sortBy: JSON::dump([['status', 'desc']]) } }
let(:expected) { { sort_by: [['status', 'desc']] } }
end
end
context 'as sortBy in colon concatenated value' do
it_behaves_like 'transforms' do
let(:params) { { sortBy: JSON::dump(['status:desc']) } }
let(:expected) { { sort_by: [['status', 'desc']] } }
end
end
context 'with an invalid JSON' do
let(:params) { { sortBy: 'faulty' + JSON::dump(['status:desc']) } }
it 'is not success' do
expect(subject)
.to_not be_success
end
it 'returns the error' do
message = 'unexpected token at \'faulty["status:desc"]\''
expect(subject.errors.messages[:base].length)
.to eql(1)
expect(subject.errors.messages[:base][0])
.to end_with(message)
end
end
end
context 'with filters' do
context 'as filters in dumped json' do
context 'with a filter named internally' do
it_behaves_like 'transforms' do
let(:params) do
{ filters: JSON::dump([{ 'status_id' => { 'operator' => '=',
'values' => ['1', '2'] } }]) }
end
let(:expected) do
{ filters: [{ field: 'status_id', operator: '=', values: ['1', '2'] }] }
end
end
end
context 'with a filter named according to v3' do
it_behaves_like 'transforms' do
let(:params) do
{ filters: JSON::dump([{ 'status' => { 'operator' => '=',
'values' => ['1', '2'] } }]) }
end
let(:expected) do
{ filters: [{ field: 'status_id', operator: '=', values: ['1', '2'] }] }
end
end
it_behaves_like 'transforms' do
let(:params) do
{ filters: JSON::dump([{ 'subprojectId' => { 'operator' => '=',
'values' => ['1', '2'] } }]) }
end
let(:expected) do
{ filters: [{ field: 'subproject_id', operator: '=', values: ['1', '2'] }] }
end
end
it_behaves_like 'transforms' do
let(:params) do
{ filters: JSON::dump([{ 'watcher' => { 'operator' => '=',
'values' => ['1', '2'] } }]) }
end
let(:expected) do
{ filters: [{ field: 'watcher_id', operator: '=', values: ['1', '2'] }] }
end
end
it_behaves_like 'transforms' do
let(:params) do
{ filters: JSON::dump([{ 'custom_field_1' => { 'operator' => '=',
'values' => ['1', '2'] } }]) }
end
let(:expected) do
{ filters: [{ field: 'cf_1', operator: '=', values: ['1', '2'] }] }
end
end
end
context 'with an invalid JSON' do
let(:params) do
{ filters: 'faulty' + JSON::dump([{ 'status' => { 'operator' => '=',
'values' => ['1', '2'] } }]) }
end
it 'is not success' do
expect(subject)
.to_not be_success
end
it 'returns the error' do
message = 'unexpected token at ' +
"'faulty[{\"status\":{\"operator\":\"=\",\"values\":[\"1\",\"2\"]}}]'"
expect(subject.errors.messages[:base].length)
.to eql(1)
expect(subject.errors.messages[:base][0])
.to end_with(message)
end
end
end
end
context 'with showSums' do
it_behaves_like 'transforms' do
let(:params) { { showSums: 'true' } }
let(:expected) { { display_sums: true } }
end
it_behaves_like 'transforms' do
let(:params) { { showSums: 'false' } }
let(:expected) { { display_sums: false } }
end
end
end
end

@ -0,0 +1,114 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe ::API::V3::UpdateQueryFromV3ParamsService,
type: :model do
let(:user) { FactoryGirl.build_stubbed(:user) }
let(:query) { FactoryGirl.build_stubbed(:query) }
let(:params) { double('params') }
let(:parsed_params) { double('parsed_params') }
let(:mock_parse_query_service) do
mock = double('ParseQueryParamsService')
allow(mock)
.to receive(:call)
.with(params)
.and_return(mock_parse_query_service_response)
mock
end
let(:mock_parse_query_service_response) do
ServiceResult.new(success: mock_parse_query_service_success,
errors: mock_parse_query_service_errors,
result: mock_parse_query_service_result)
end
let(:mock_parse_query_service_success) { true }
let(:mock_parse_query_service_errors) { nil }
let(:mock_parse_query_service_result) { parsed_params }
let(:mock_update_query_service) do
mock = double('UpdateQueryFromParamsService')
allow(mock)
.to receive(:call)
.with(parsed_params)
.and_return(mock_update_query_service_response)
mock
end
let(:mock_update_query_service_response) do
ServiceResult.new(success: mock_update_query_service_success,
errors: mock_update_query_service_errors,
result: mock_update_query_service_result)
end
let(:mock_update_query_service_success) { true }
let(:mock_update_query_service_errors) { nil }
let(:mock_update_query_service_result) { query }
let(:instance) { described_class.new(query, user) }
before do
allow(UpdateQueryFromParamsService)
.to receive(:new)
.with(query, user)
.and_return(mock_update_query_service)
allow(::API::V3::ParseQueryParamsService)
.to receive(:new)
.with(no_args)
.and_return(mock_parse_query_service)
end
describe '#call' do
subject { instance.call(params) }
it 'returns the update result' do
is_expected
.to eql(mock_update_query_service_response)
end
context 'when parsing fails' do
let(:mock_parse_query_service_success) { false }
let(:mock_parse_query_service_errors) { double 'error' }
let(:mock_parse_query_service_result) { nil }
it 'returns the parse result' do
is_expected
.to eql(mock_parse_query_service_response)
end
end
end
end

@ -0,0 +1,89 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe ::API::V3::WorkPackageCollectionFromQueryParamsService,
type: :model do
include API::V3::Utilities::PathHelper
let(:mock_wp_collection_from_query_service) do
mock = double('WorkPackageCollectionFromQueryService')
allow(mock)
.to receive(:call)
.with(params)
.and_return(mock_wp_collection_service_response)
mock
end
let(:mock_wp_collection_service_response) do
ServiceResult.new(success: mock_wp_collection_service_success,
errors: mock_wp_collection_service_errors,
result: mock_wp_collection_service_result)
end
let(:mock_wp_collection_service_success) { true }
let(:mock_wp_collection_service_errors) { nil }
let(:mock_wp_collection_service_result) { double('result') }
let(:query) { FactoryGirl.build_stubbed(:query) }
let(:project) { FactoryGirl.build_stubbed(:project) }
let(:user) { FactoryGirl.build_stubbed(:user) }
let(:instance) { described_class.new(user) }
before do
stub_const('::API::V3::WorkPackageCollectionFromQueryService',
mock_wp_collection_from_query_service)
allow(::API::V3::WorkPackageCollectionFromQueryService)
.to receive(:new)
.with(query, user)
.and_return(mock_wp_collection_from_query_service)
end
describe '#call' do
let(:params) { { project: project } }
subject { instance.call(params) }
before do
allow(Query)
.to receive(:new)
.with(name: '_', project: project, sort_criteria: [['parent', 'desc']])
.and_return(query)
end
it 'is successful' do
is_expected
.to eql(mock_wp_collection_service_response)
end
end
end

@ -0,0 +1,383 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe ::API::V3::WorkPackageCollectionFromQueryService,
type: :model do
include API::V3::Utilities::PathHelper
let(:query) do
query = FactoryGirl.build_stubbed(:query)
allow(query)
.to receive(:results)
.and_return(results)
query
end
let(:results) do
results = double('results')
allow(results)
.to receive(:sorted_work_packages)
.and_return([work_package])
allow(results)
.to receive(:all_total_sums)
.and_return(OpenStruct.new(name: :estimated_hours) => 0.0)
allow(results)
.to receive(:work_package_count_by_group)
.and_return(1 => 5, 2 => 10)
allow(results)
.to receive(:all_sums_for_group)
.with(1)
.and_return(OpenStruct.new(name: :status_id) => 50)
allow(results)
.to receive(:all_sums_for_group)
.with(2)
.and_return(OpenStruct.new(name: :status_id) => 100)
results
end
let(:user) { FactoryGirl.build_stubbed(:user) }
let(:project) { FactoryGirl.build_stubbed(:project) }
let(:mock_wp_representer) do
Struct.new(:work_packages,
:self_link,
:query,
:project,
:groups,
:total_sums,
:page,
:per_page,
:embed_schemas,
:current_user) do
def initialize(work_packages,
self_link,
query:,
project:,
groups:,
total_sums:,
page:,
per_page:,
embed_schemas:,
current_user:)
super(work_packages,
self_link,
query,
project,
groups,
total_sums,
page,
per_page,
embed_schemas,
current_user)
end
end
end
let(:mock_aggregation_representer) do
Struct.new(:group,
:count,
:sums) do
def initialize(group,
count,
sums:)
super(group,
count,
sums)
end
end
end
let(:params) { {} }
let(:mock_update_query_service) do
mock = double('UpdateQueryFromV3ParamsService')
allow(mock)
.to receive(:call)
.with(params)
.and_return(mock_update_query_service_response)
mock
end
let(:mock_update_query_service_response) do
ServiceResult.new(success: update_query_service_success,
errors: update_query_service_errors,
result: update_query_service_result)
end
let(:update_query_service_success) { true }
let(:update_query_service_errors) { nil }
let(:update_query_service_result) { query }
let(:work_package) { FactoryGirl.build_stubbed(:work_package) }
let(:instance) { described_class.new(query, user) }
describe '#call' do
subject { instance.call(params) }
it 'is successful' do
is_expected
.to be_success
end
before do
stub_const('::API::V3::WorkPackages::WorkPackageCollectionRepresenter', mock_wp_representer)
stub_const('::API::Decorators::AggregationGroup', mock_aggregation_representer)
allow(::API::V3::UpdateQueryFromV3ParamsService)
.to receive(:new)
.with(query, user)
.and_return(mock_update_query_service)
end
context 'result' do
subject { instance.call(params).result }
it 'is a WorkPackageCollectionRepresenter' do
is_expected
.to be_a(::API::V3::WorkPackages::WorkPackageCollectionRepresenter)
end
context 'work_packages' do
it "has the querie's work_package results set" do
expect(subject.work_packages)
.to match_array([work_package])
end
end
context 'current_user' do
it 'has the provided user set' do
expect(subject.current_user)
.to eq(user)
end
end
context 'project' do
it 'has the queries project set' do
expect(subject.project)
.to eq(query.project)
end
end
context 'self_link' do
context 'if the project is nil' do
let(:query) { FactoryGirl.build_stubbed(:query, project: nil) }
it 'is the global work_package link' do
expect(subject.self_link)
.to eq(api_v3_paths.work_packages)
end
end
context 'if the project is set' do
let(:query) { FactoryGirl.build_stubbed(:query, project: project) }
it 'is the global work_package link' do
expect(subject.self_link)
.to eq(api_v3_paths.work_packages_by_project(project.id))
end
end
end
context 'embed_schemas' do
it 'is true' do
expect(subject.embed_schemas)
.to be_truthy
end
end
context 'total_sums' do
context 'with query.display_sums? being false' do
it 'is nil' do
query.display_sums = false
expect(subject.total_sums)
.to be_nil
end
end
context 'with query.display_sums? being true' do
it 'has a struct containg the sums' do
query.display_sums = true
expected = OpenStruct.new(estimated_hours: 0.0)
expect(subject.total_sums)
.to eq(expected)
end
end
end
context 'groups' do
context 'with query.grouped? being false' do
it 'is nil' do
query.group_by = nil
expect(subject.groups)
.to be_nil
end
end
context 'with query.group_by being empty' do
it 'is nil' do
query.group_by = ''
expect(subject.groups)
.to be_nil
end
end
context 'with query.grouped? being true' do
it 'has the groups' do
query.group_by = 'status'
expect(subject.groups[0].group)
.to eq(1)
expect(subject.groups[0].count)
.to eq(5)
expect(subject.groups[1].group)
.to eq(2)
expect(subject.groups[1].count)
.to eq(10)
end
end
end
context 'query (in the url)' do
context 'when displaying sums' do
it 'is represented' do
query.display_sums = true
expect(subject.query[:showSums])
.to eq('true')
end
end
context 'when grouping' do
it 'is represented' do
query.group_by = 'status_id'
expect(subject.query[:groupBy])
.to eq('status_id')
end
end
context 'when sorting' do
it 'is represented' do
query.sort_criteria = [['status_id', 'desc']]
expected_sort = JSON::dump [['status', 'desc']]
expect(subject.query[:sortBy])
.to eq(expected_sort)
end
end
context 'filters' do
it 'is represented' do
query.add_filter('status_id', '=', ['1', '2'])
query.add_filter('subproject_id', '=', ['3', '4'])
expected_filters = JSON::dump([
{ status: { operator: '=', values: ['1', '2'] } },
{ subprojectId: { operator: '=', values: ['3', '4'] } }
])
expect(subject.query[:filters])
.to eq(expected_filters)
end
end
end
context 'offset' do
it 'is 1 as default' do
expect(subject.query[:offset])
.to be(1)
end
context 'with a provided value' do
# It is imporant for the keys to be strings
# as that is what will come from the client
let(:params) { { 'offset' => 3 } }
it 'is that value' do
expect(subject.query[:offset])
.to be(3)
end
end
end
context 'pageSize' do
before do
allow(Setting)
.to receive(:per_page_options_array)
.and_return([25, 50])
end
it 'is nil' do
expect(subject.query[:pageSize])
.to be(25)
end
context 'with a provided value' do
# It is imporant for the keys to be strings
# as that is what will come from the client
let(:params) { { 'pageSize' => 100 } }
it 'is that value' do
expect(subject.query[:pageSize])
.to be(100)
end
end
end
end
context 'when the update query service fails' do
let(:update_query_service_success) { false }
let(:update_query_service_errors) { double('errors') }
let(:update_query_service_result) { nil }
it 'returns the update service response' do
is_expected
.to eql(mock_update_query_service_response)
end
end
end
end

@ -0,0 +1,104 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe UpdateQueryFromParamsService,
type: :model do
let(:user) { FactoryGirl.build_stubbed(:user) }
let(:query) { FactoryGirl.build_stubbed(:query) }
let(:instance) { described_class.new(query, user) }
let(:params) { {} }
describe '#call' do
subject { instance.call(params) }
context 'group_by' do
context 'for an existing value' do
let(:params) { { group_by: 'status' } }
it 'sets the value' do
subject
expect(query.group_by)
.to eql('status')
end
end
end
context 'filters' do
let(:params) do
{ filters: [{ field: 'status_id', operator: '=', values: ['1', '2'] }] }
end
context 'for a valid filter' do
it 'sets the filter' do
subject
expect(query.filters.length)
.to eql(1)
expect(query.filters[0].name)
.to eql(:status_id)
expect(query.filters[0].operator)
.to eql('=')
expect(query.filters[0].values)
.to eql(['1', '2'])
end
end
end
context 'sort_by' do
let(:params) do
{ sort_by: [['status_id', 'desc']] }
end
it 'sets the order' do
subject
expect(query.sort_criteria)
.to eql([['status_id', 'desc']])
end
end
context 'columns' do
let(:params) do
{ columns: ['assigned_to', 'author', 'category', 'subject'] }
end
it 'sets the columns' do
subject
expect(query.column_names)
.to match_array(params[:columns].map(&:to_sym))
end
end
end
end

@ -235,3 +235,19 @@ shared_examples_for 'list_optional query filter' do
end
end
end
shared_examples_for 'non ar filter' do
describe '#ar_object_filter?' do
it 'is false' do
expect(instance)
.not_to be_ar_object_filter
end
end
describe '#value_objects' do
it 'is empty' do
expect(instance.value_objects)
.to be_empty
end
end
end

Loading…
Cancel
Save