From c4a8320ed5796e9c09f13e8c21bdf27e3206b717 Mon Sep 17 00:00:00 2001 From: Wieland Lindenthal Date: Mon, 23 Jan 2017 17:57:56 +0100 Subject: [PATCH 01/16] Fix: logic for `ee_manager_visible` is falsey `allows_to?` and `show_banners?` should behave differently if the configuration is set to false. --- app/models/enterprise_token.rb | 4 ++-- .../authorization/enterprise_service.rb | 2 +- config/initializers/homescreen.rb | 2 +- lib/open_project/configuration.rb | 2 +- .../menu_manager/top_menu/help_menu.rb | 2 +- spec/models/enterprise_token_spec.rb | 24 +++++++++++++++---- 6 files changed, 26 insertions(+), 10 deletions(-) diff --git a/app/models/enterprise_token.rb b/app/models/enterprise_token.rb index 076ad1bb45..dbf64ad575 100644 --- a/app/models/enterprise_token.rb +++ b/app/models/enterprise_token.rb @@ -37,8 +37,8 @@ class EnterpriseToken < ActiveRecord::Base Authorization::EnterpriseService.new(current).call(action).result end - def show_banners - !current || current.expired? + def show_banners? + OpenProject::Configuration.ee_manager_visible? && (!current || current.expired?) end def set_current_token diff --git a/app/services/authorization/enterprise_service.rb b/app/services/authorization/enterprise_service.rb index 3501f5500e..fcbf4b1041 100644 --- a/app/services/authorization/enterprise_service.rb +++ b/app/services/authorization/enterprise_service.rb @@ -37,7 +37,7 @@ class Authorization::EnterpriseService # Return a true ServiceResult if the token contains this particular action. def call(action) allowed = - if token.nil? || token.token_object.nil? || token.expired? + if OpenProject::Configuration.ee_manager_visible? && (token.nil? || token.token_object.nil? || token.expired?) false else process(action) diff --git a/config/initializers/homescreen.rb b/config/initializers/homescreen.rb index 945ca1a946..a4defac299 100644 --- a/config/initializers/homescreen.rb +++ b/config/initializers/homescreen.rb @@ -45,7 +45,7 @@ OpenProject::Static::Homescreen.manage :blocks do |blocks| { partial: 'administration', if: Proc.new { User.current.admin? } }, { partial: 'upsale', - if: Proc.new { EnterpriseToken.show_banners } } + if: Proc.new { EnterpriseToken.show_banners? } } ) end diff --git a/lib/open_project/configuration.rb b/lib/open_project/configuration.rb index abede9db22..f5c57248e0 100644 --- a/lib/open_project/configuration.rb +++ b/lib/open_project/configuration.rb @@ -95,7 +95,7 @@ module OpenProject 'onboarding_video_url' => 'https://player.vimeo.com/video/163426858?autoplay=1', 'onboarding_enabled' => true, - 'ee_manager_visible' => true + 'ee_manager_visible' => false } @config = nil diff --git a/lib/redmine/menu_manager/top_menu/help_menu.rb b/lib/redmine/menu_manager/top_menu/help_menu.rb index e32ed810e4..ff2fb67b75 100644 --- a/lib/redmine/menu_manager/top_menu/help_menu.rb +++ b/lib/redmine/menu_manager/top_menu/help_menu.rb @@ -83,7 +83,7 @@ module Redmine::MenuManager::TopMenu::HelpMenu class: 'drop-down--help-headline', title: l('top_menu.help_and_support') } - if EnterpriseToken.show_banners + if EnterpriseToken.show_banners? result << static_link_item(:upsale, href_suffix: "?utm_source=ce-helpmenu") end result << static_link_item(:user_guides) diff --git a/spec/models/enterprise_token_spec.rb b/spec/models/enterprise_token_spec.rb index 50b92d1ba9..1f3610c598 100644 --- a/spec/models/enterprise_token_spec.rb +++ b/spec/models/enterprise_token_spec.rb @@ -6,6 +6,7 @@ RSpec.describe EnterpriseToken, type: :model do before do RequestStore.delete :current_ee_token + allow(OpenProject::Configuration).to receive(:ee_manager_visible?).and_return(true) end describe 'existing token' do @@ -20,7 +21,7 @@ RSpec.describe EnterpriseToken, type: :model do expect(EnterpriseToken.count).to eq(1) expect(EnterpriseToken.current).to eq(subject) expect(EnterpriseToken.current.encoded_token).to eq('foo') - expect(EnterpriseToken.show_banners).to eq(false) + expect(EnterpriseToken.show_banners?).to eq(false) # Deleting it updates the current token EnterpriseToken.current.destroy! @@ -74,7 +75,7 @@ RSpec.describe EnterpriseToken, type: :model do it 'has an expired token' do expect(EnterpriseToken.current).to eq(subject) - expect(EnterpriseToken.show_banners).to eq(true) + expect(EnterpriseToken.show_banners?).to eq(true) end end @@ -89,14 +90,29 @@ RSpec.describe EnterpriseToken, type: :model do describe 'no token' do it do expect(EnterpriseToken.current).to be_nil - expect(EnterpriseToken.show_banners).to eq(true) + expect(EnterpriseToken.show_banners?).to eq(true) end end describe 'invalid token' do it 'appears as if no token is shown' do expect(EnterpriseToken.current).to be_nil - expect(EnterpriseToken.show_banners).to eq(true) + expect(EnterpriseToken.show_banners?).to eq(true) end end + + describe "Configuration file has `ee_manager_visible` set to false" do + before do + expect(OpenProject::Configuration).to receive(:ee_manager_visible?).and_return(false) + end + + it 'allows all EE features' do + expect(EnterpriseToken.allows_to?(:define_custom_style)).to be_truthy + end + + it 'does not show banners promoting EE' do + expect(EnterpriseToken.show_banners?).to be_falsey + end + end + end From 7c90a92a755285c6a7e43cad1e05c860f8914768 Mon Sep 17 00:00:00 2001 From: Wieland Lindenthal Date: Mon, 23 Jan 2017 18:07:07 +0100 Subject: [PATCH 02/16] Fix: undo accidental changing the default of `ee_manager_visible` --- lib/open_project/configuration.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/open_project/configuration.rb b/lib/open_project/configuration.rb index f5c57248e0..abede9db22 100644 --- a/lib/open_project/configuration.rb +++ b/lib/open_project/configuration.rb @@ -95,7 +95,7 @@ module OpenProject 'onboarding_video_url' => 'https://player.vimeo.com/video/163426858?autoplay=1', 'onboarding_enabled' => true, - 'ee_manager_visible' => false + 'ee_manager_visible' => true } @config = nil From 6cd14ee7a5041eb1f1320943c3c897e9a0eb548e Mon Sep 17 00:00:00 2001 From: Wieland Lindenthal Date: Thu, 26 Jan 2017 10:04:21 +0100 Subject: [PATCH 03/16] Untangle logic. `ee_manager_visible` only effects visibility of banners advertising EE. It does not effect `allows_to?` checks on the token. --- app/services/authorization/enterprise_service.rb | 2 +- spec/models/enterprise_token_spec.rb | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/app/services/authorization/enterprise_service.rb b/app/services/authorization/enterprise_service.rb index fcbf4b1041..3501f5500e 100644 --- a/app/services/authorization/enterprise_service.rb +++ b/app/services/authorization/enterprise_service.rb @@ -37,7 +37,7 @@ class Authorization::EnterpriseService # Return a true ServiceResult if the token contains this particular action. def call(action) allowed = - if OpenProject::Configuration.ee_manager_visible? && (token.nil? || token.token_object.nil? || token.expired?) + if token.nil? || token.token_object.nil? || token.expired? false else process(action) diff --git a/spec/models/enterprise_token_spec.rb b/spec/models/enterprise_token_spec.rb index 1f3610c598..47f76c4d75 100644 --- a/spec/models/enterprise_token_spec.rb +++ b/spec/models/enterprise_token_spec.rb @@ -102,15 +102,9 @@ RSpec.describe EnterpriseToken, type: :model do end describe "Configuration file has `ee_manager_visible` set to false" do - before do - expect(OpenProject::Configuration).to receive(:ee_manager_visible?).and_return(false) - end - - it 'allows all EE features' do - expect(EnterpriseToken.allows_to?(:define_custom_style)).to be_truthy - end it 'does not show banners promoting EE' do + expect(OpenProject::Configuration).to receive(:ee_manager_visible?).and_return(false) expect(EnterpriseToken.show_banners?).to be_falsey end end From 5135c13545d755eb3d398e705d2aed8bd772f4bd Mon Sep 17 00:00:00 2001 From: Jens Ulferts Date: Wed, 1 Feb 2017 16:59:23 +0100 Subject: [PATCH 04/16] add stub for ee base layout rendering --- spec/views/layouts/base.html.erb_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/views/layouts/base.html.erb_spec.rb b/spec/views/layouts/base.html.erb_spec.rb index a8b7711b7e..dbc096ac6d 100644 --- a/spec/views/layouts/base.html.erb_spec.rb +++ b/spec/views/layouts/base.html.erb_spec.rb @@ -196,6 +196,7 @@ describe 'layouts/base', type: :view do context "EE is active and styles are not present" do before do allow(EnterpriseToken).to receive(:current).and_return(a_token) + allow(a_token).to receive(:expired?).and_return(false) allow(a_token).to receive(:allows_to?).with(:define_custom_style).and_return(true) allow(CustomStyle).to receive(:current).and_return(nil) From 80c242dd53ed332733afb48c5f55afd98efdbb69 Mon Sep 17 00:00:00 2001 From: Jens Ulferts Date: Tue, 24 Jan 2017 10:49:54 +0100 Subject: [PATCH 05/16] embed query results in query representer This is only done when the representer is not embedded itself, e.g. when it is part of a collection. --- .../api/experimental/concerns/v3_naming.rb | 18 +- app/helpers/queries_helper.rb | 44 +- app/models/queries/available_filters.rb | 18 +- app/models/queries/filter_serializer.rb | 2 +- app/models/query.rb | 8 + .../api/v3/parse_query_params_service.rb | 167 ++++++++ .../v3/update_query_from_v3_params_service.rb | 55 +++ ...ge_collection_from_query_params_service.rb | 49 +++ ...k_package_collection_from_query_service.rb | 155 +++++++ .../update_query_from_params_service.rb | 84 ++++ lib/api/root.rb | 14 + lib/api/utilities/property_name_converter.rb | 14 +- ...perty_name_converter_work_package_dummy.rb | 38 ++ .../utilities/query_filters_name_converter.rb | 47 +++ .../query_filters_name_converter_context.rb | 9 + .../utilities/wp_property_name_converter.rb | 47 +++ lib/api/v3/queries/queries_api.rb | 10 +- .../v3/queries/query_params_representer.rb | 90 ++++ lib/api/v3/queries/query_representer.rb | 39 ++ .../work_package_list_helpers.rb | 210 ---------- lib/api/v3/work_packages/work_packages_api.rb | 15 +- .../work_packages_by_project_api.rb | 11 +- .../utilities/property_name_converter_spec.rb | 9 + .../api/v3/queries/query_representer_spec.rb | 125 +++++- spec/models/queries/available_filters_spec.rb | 2 +- .../api/v3/queries/query_resource_spec.rb | 11 + .../api/v3/parse_query_params_service_spec.rb | 231 +++++++++++ ...pdate_query_from_v3_params_service_spec.rb | 114 ++++++ ...llection_from_query_params_service_spec.rb | 89 ++++ ...kage_collection_from_query_service_spec.rb | 383 ++++++++++++++++++ .../update_query_from_params_service.rb | 104 +++++ 31 files changed, 1938 insertions(+), 274 deletions(-) create mode 100644 app/services/api/v3/parse_query_params_service.rb create mode 100644 app/services/api/v3/update_query_from_v3_params_service.rb create mode 100644 app/services/api/v3/work_package_collection_from_query_params_service.rb create mode 100644 app/services/api/v3/work_package_collection_from_query_service.rb create mode 100644 app/services/update_query_from_params_service.rb create mode 100644 lib/api/utilities/property_name_converter_work_package_dummy.rb create mode 100644 lib/api/utilities/query_filters_name_converter.rb create mode 100644 lib/api/utilities/query_filters_name_converter_context.rb create mode 100644 lib/api/utilities/wp_property_name_converter.rb create mode 100644 lib/api/v3/queries/query_params_representer.rb delete mode 100644 lib/api/v3/work_packages/work_package_list_helpers.rb create mode 100644 spec/services/api/v3/parse_query_params_service_spec.rb create mode 100644 spec/services/api/v3/update_query_from_v3_params_service_spec.rb create mode 100644 spec/services/api/v3/work_package_collection_from_query_params_service_spec.rb create mode 100644 spec/services/api/v3/work_package_collection_from_query_service_spec.rb create mode 100644 spec/services/update_query_from_params_service.rb diff --git a/app/controllers/api/experimental/concerns/v3_naming.rb b/app/controllers/api/experimental/concerns/v3_naming.rb index 4b701c3cd4..f2052b9c17 100644 --- a/app/controllers/api/experimental/concerns/v3_naming.rb +++ b/app/controllers/api/experimental/concerns/v3_naming.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 diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index 9317531ed5..033752e6ab 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -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 diff --git a/app/models/queries/available_filters.rb b/app/models/queries/available_filters.rb index 7a54a2e1d9..9d0075c934 100644 --- a/app/models/queries/available_filters.rb +++ b/app/models/queries/available_filters.rb @@ -28,6 +28,16 @@ #++ module Queries::AvailableFilters + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def registered_filters + Queries::Register.filters[self] + end + end + def available_filters uninitialized = registered_filters - already_initialized_filters @@ -86,15 +96,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 diff --git a/app/models/queries/filter_serializer.rb b/app/models/queries/filter_serializer.rb index 6e4c62e867..bd7057bdf7 100644 --- a/app/models/queries/filter_serializer.rb +++ b/app/models/queries/filter_serializer.rb @@ -46,7 +46,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 diff --git a/app/models/query.rb b/app/models/query.rb index 38e83d265c..e4be982433 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -246,6 +246,10 @@ class Query < ActiveRecord::Base filter end + def filtered? + filters.any? + end + def normalized_name name.parameterize.underscore end @@ -360,6 +364,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) diff --git a/app/services/api/v3/parse_query_params_service.rb b/app/services/api/v3/parse_query_params_service.rb new file mode 100644 index 0000000000..9a7081a401 --- /dev/null +++ b/app/services/api/v3/parse_query_params_service.rb @@ -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 diff --git a/app/services/api/v3/update_query_from_v3_params_service.rb b/app/services/api/v3/update_query_from_v3_params_service.rb new file mode 100644 index 0000000000..7b6b9bc2fc --- /dev/null +++ b/app/services/api/v3/update_query_from_v3_params_service.rb @@ -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 diff --git a/app/services/api/v3/work_package_collection_from_query_params_service.rb b/app/services/api/v3/work_package_collection_from_query_params_service.rb new file mode 100644 index 0000000000..b59479087d --- /dev/null +++ b/app/services/api/v3/work_package_collection_from_query_params_service.rb @@ -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 diff --git a/app/services/api/v3/work_package_collection_from_query_service.rb b/app/services/api/v3/work_package_collection_from_query_service.rb new file mode 100644 index 0000000000..542e93b5c6 --- /dev/null +++ b/app/services/api/v3/work_package_collection_from_query_service.rb @@ -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 diff --git a/app/services/update_query_from_params_service.rb b/app/services/update_query_from_params_service.rb new file mode 100644 index 0000000000..80ce740287 --- /dev/null +++ b/app/services/update_query_from_params_service.rb @@ -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 diff --git a/lib/api/root.rb b/lib/api/root.rb index a5c4b7a68e..03d908c486 100644 --- a/lib/api/root.rb +++ b/lib/api/root.rb @@ -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 diff --git a/lib/api/utilities/property_name_converter.rb b/lib/api/utilities/property_name_converter.rb index e1eb004133..1ca813b51c 100644 --- a/lib/api/utilities/property_name_converter.rb +++ b/lib/api/utilities/property_name_converter.rb @@ -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 diff --git a/lib/api/utilities/property_name_converter_work_package_dummy.rb b/lib/api/utilities/property_name_converter_work_package_dummy.rb new file mode 100644 index 0000000000..fb6acab148 --- /dev/null +++ b/lib/api/utilities/property_name_converter_work_package_dummy.rb @@ -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 diff --git a/lib/api/utilities/query_filters_name_converter.rb b/lib/api/utilities/query_filters_name_converter.rb new file mode 100644 index 0000000000..d5a7261c49 --- /dev/null +++ b/lib/api/utilities/query_filters_name_converter.rb @@ -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 diff --git a/lib/api/utilities/query_filters_name_converter_context.rb b/lib/api/utilities/query_filters_name_converter_context.rb new file mode 100644 index 0000000000..bbbc55cbfe --- /dev/null +++ b/lib/api/utilities/query_filters_name_converter_context.rb @@ -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 diff --git a/lib/api/utilities/wp_property_name_converter.rb b/lib/api/utilities/wp_property_name_converter.rb new file mode 100644 index 0000000000..f254657106 --- /dev/null +++ b/lib/api/utilities/wp_property_name_converter.rb @@ -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 diff --git a/lib/api/v3/queries/queries_api.rb b/lib/api/v3/queries/queries_api.rb index 2e19b13e76..306d4ef485 100644 --- a/lib/api/v3/queries/queries_api.rb +++ b/lib/api/v3/queries/queries_api.rb @@ -58,7 +58,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 diff --git a/lib/api/v3/queries/query_params_representer.rb b/lib/api/v3/queries/query_params_representer.rb new file mode 100644 index 0000000000..e5fc006c67 --- /dev/null +++ b/lib/api/v3/queries/query_params_representer.rb @@ -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 diff --git a/lib/api/v3/queries/query_representer.rb b/lib/api/v3/queries/query_representer.rb index 60eae2fd8a..7da9de169d 100644 --- a/lib/api/v3/queries/query_representer.rb +++ b/lib/api/v3/queries/query_representer.rb @@ -36,6 +36,37 @@ 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 + linked_property :user linked_property :project @@ -78,6 +109,14 @@ module API self.to_eager_load = [:query_menu_item, project: { work_package_custom_fields: :translations }] + property :results, + exec_context: :decorator, + render_nil: true, + embedded: true, + if: ->(*) { + results + } + private def convert_attribute(attribute) diff --git a/lib/api/v3/work_packages/work_package_list_helpers.rb b/lib/api/v3/work_packages/work_package_list_helpers.rb deleted file mode 100644 index fee329fc10..0000000000 --- a/lib/api/v3/work_packages/work_package_list_helpers.rb +++ /dev/null @@ -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 diff --git a/lib/api/v3/work_packages/work_packages_api.rb b/lib/api/v3/work_packages/work_packages_api.rb index cc59e2e95b..b64cc08adc 100644 --- a/lib/api/v3/work_packages/work_packages_api.rb +++ b/lib/api/v3/work_packages/work_packages_api.rb @@ -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 diff --git a/lib/api/v3/work_packages/work_packages_by_project_api.rb b/lib/api/v3/work_packages/work_packages_by_project_api.rb index 6bdf848883..d2cfb1f529 100644 --- a/lib/api/v3/work_packages/work_packages_by_project_api.rb +++ b/lib/api/v3/work_packages/work_packages_by_project_api.rb @@ -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 diff --git a/spec/lib/api/utilities/property_name_converter_spec.rb b/spec/lib/api/utilities/property_name_converter_spec.rb index 686772ebc8..bfca478789 100644 --- a/spec/lib/api/utilities/property_name_converter_spec.rb +++ b/spec/lib/api/utilities/property_name_converter_spec.rb @@ -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 diff --git a/spec/lib/api/v3/queries/query_representer_spec.rb b/spec/lib/api/v3/queries/query_representer_spec.rb index 66428c4958..d0cbdcf23a 100644 --- a/spec/lib/api/v3/queries/query_representer_spec.rb +++ b/spec/lib/api/v3/queries/query_representer_spec.rb @@ -31,9 +31,8 @@ require 'spec_helper' describe ::API::V3::Queries::QueryRepresenter do include ::API::V3::Utilities::PathHelper - let(:query) { - FactoryGirl.build_stubbed(:query) - } + let(:query) { FactoryGirl.build_stubbed(:query, project: project) } + let(:project) { FactoryGirl.build_stubbed(:project) } let(:representer) { described_class.new(query, current_user: double('current_user')) } subject { representer.to_json } @@ -58,12 +57,89 @@ 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_id', 'asc'], ['type_id', '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 end @@ -120,16 +196,15 @@ describe ::API::V3::Queries::QueryRepresenter do 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') + is_expected + .to be_json_eql([['subject', 'asc'], ['assignee', 'desc']].to_json) + .at_path('sortCriteria') end end @@ -140,5 +215,37 @@ describe ::API::V3::Queries::QueryRepresenter do is_expected.to be_json_eql(['subject', 'assignee'].to_json).at_path('columnNames') 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 end diff --git a/spec/models/queries/available_filters_spec.rb b/spec/models/queries/available_filters_spec.rb index dccc8b05c1..370254fda8 100644 --- a/spec/models/queries/available_filters_spec.rb +++ b/spec/models/queries/available_filters_spec.rb @@ -46,7 +46,7 @@ describe Queries::AvailableFilters, type: :model do includer = HelperClass.new(context) allow(includer) - .to receive(:filter_register) + .to receive(:registered_filters) .and_return(registered_filters) includer diff --git a/spec/requests/api/v3/queries/query_resource_spec.rb b/spec/requests/api/v3/queries/query_resource_spec.rb index f61ae4777a..095f53b1d3 100644 --- a/spec/requests/api/v3/queries/query_resource_spec.rb +++ b/spec/requests/api/v3/queries/query_resource_spec.rb @@ -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 diff --git a/spec/services/api/v3/parse_query_params_service_spec.rb b/spec/services/api/v3/parse_query_params_service_spec.rb new file mode 100644 index 0000000000..38549516e4 --- /dev/null +++ b/spec/services/api/v3/parse_query_params_service_spec.rb @@ -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 diff --git a/spec/services/api/v3/update_query_from_v3_params_service_spec.rb b/spec/services/api/v3/update_query_from_v3_params_service_spec.rb new file mode 100644 index 0000000000..158dc338e4 --- /dev/null +++ b/spec/services/api/v3/update_query_from_v3_params_service_spec.rb @@ -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 diff --git a/spec/services/api/v3/work_package_collection_from_query_params_service_spec.rb b/spec/services/api/v3/work_package_collection_from_query_params_service_spec.rb new file mode 100644 index 0000000000..8cc3a12597 --- /dev/null +++ b/spec/services/api/v3/work_package_collection_from_query_params_service_spec.rb @@ -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 diff --git a/spec/services/api/v3/work_package_collection_from_query_service_spec.rb b/spec/services/api/v3/work_package_collection_from_query_service_spec.rb new file mode 100644 index 0000000000..4804a92e4b --- /dev/null +++ b/spec/services/api/v3/work_package_collection_from_query_service_spec.rb @@ -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 diff --git a/spec/services/update_query_from_params_service.rb b/spec/services/update_query_from_params_service.rb new file mode 100644 index 0000000000..2f7abcbcff --- /dev/null +++ b/spec/services/update_query_from_params_service.rb @@ -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 From 3f2c0e6a3d3ba2730e0d02bca3141bf0ef91f29a Mon Sep 17 00:00:00 2001 From: Jens Ulferts Date: Thu, 26 Jan 2017 16:52:39 +0100 Subject: [PATCH 06/16] link to column names --- app/models/query.rb | 7 ++ lib/api/decorators/single.rb | 9 +- .../columns/query_column_representer.rb | 67 ++++++++++++++ .../v3/queries/columns/query_columns_api.rb | 66 ++++++++++++++ lib/api/v3/queries/queries_api.rb | 2 + lib/api/v3/queries/query_representer.rb | 31 +++++-- lib/api/v3/utilities/path_helper.rb | 4 + .../columns/query_column_representer_spec.rb | 90 +++++++++++++++++++ .../api/v3/queries/query_representer_spec.rb | 78 +++++++++++++++- spec/lib/api/v3/utilities/path_helper_spec.rb | 6 ++ .../columns/query_columns_resource_spec.rb | 82 +++++++++++++++++ 11 files changed, 429 insertions(+), 13 deletions(-) create mode 100644 lib/api/v3/queries/columns/query_column_representer.rb create mode 100644 lib/api/v3/queries/columns/query_columns_api.rb create mode 100644 spec/lib/api/v3/queries/columns/query_column_representer_spec.rb create mode 100644 spec/requests/api/v3/queries/columns/query_columns_resource_spec.rb diff --git a/app/models/query.rb b/app/models/query.rb index e4be982433..698afc9e29 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -272,6 +272,13 @@ 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.add_available_column(column) available_columns << column if column.is_a?(QueryColumn) end diff --git a/lib/api/decorators/single.rb b/lib/api/decorators/single.rb index 9ff2e3a018..0e5e3f89a3 100644 --- a/lib/api/decorators/single.rb +++ b/lib/api/decorators/single.rb @@ -66,7 +66,14 @@ 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)) } + + id = if id_attribute.respond_to?(:call) + instance_eval(&id_attribute) + else + represented.send(id_attribute) + end + + link_object = { href: api_v3_paths.send(path, id) } title = instance_eval(&title_getter) link_object[:title] = title if title diff --git a/lib/api/v3/queries/columns/query_column_representer.rb b/lib/api/v3/queries/columns/query_column_representer.rb new file mode 100644 index 0000000000..ee1253082e --- /dev/null +++ b/lib/api/v3/queries/columns/query_column_representer.rb @@ -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 diff --git a/lib/api/v3/queries/columns/query_columns_api.rb b/lib/api/v3/queries/columns/query_columns_api.rb new file mode 100644 index 0000000000..5656fdc8a6 --- /dev/null +++ b/lib/api/v3/queries/columns/query_columns_api.rb @@ -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 diff --git a/lib/api/v3/queries/queries_api.rb b/lib/api/v3/queries/queries_api.rb index 306d4ef485..f66493b50e 100644 --- a/lib/api/v3/queries/queries_api.rb +++ b/lib/api/v3/queries/queries_api.rb @@ -34,6 +34,8 @@ module API module Queries class QueriesAPI < ::API::OpenProjectAPI resources :queries do + mount API::V3::Queries::Columns::QueryColumnsAPI + get do authorize_any [:view_work_packages, :manage_public_queries], global: true diff --git a/lib/api/v3/queries/query_representer.rb b/lib/api/v3/queries/query_representer.rb index 7da9de169d..8cfdc17438 100644 --- a/lib/api/v3/queries/query_representer.rb +++ b/lib/api/v3/queries/query_representer.rb @@ -67,6 +67,15 @@ module API } 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 + linked_property :user linked_property :project @@ -83,12 +92,6 @@ module API end } property :is_public, getter: -> (*) { is_public } - property :column_names, - exec_context: :decorator, - getter: ->(*) { - return nil unless represented.column_names - represented.column_names.map { |name| convert_attribute name } - } property :sort_criteria, exec_context: :decorator, getter: ->(*) { @@ -106,8 +109,17 @@ module API property :display_sums, getter: -> (*) { display_sums } property :is_starred, getter: -> (*) { starred } - self.to_eager_load = [:query_menu_item, - project: { work_package_custom_fields: :translations }] + property :columns, + exec_context: :decorator, + getter: ->(*) { + represented.columns.map do |column| + ::API::V3::Queries::Columns::QueryColumnRepresenter.new(column) + end + }, + embedded: true, + if: ->(*) { + embed_links + } property :results, exec_context: :decorator, @@ -117,6 +129,9 @@ module API results } + self.to_eager_load = [:query_menu_item, + project: { work_package_custom_fields: :translations }] + private def convert_attribute(attribute) diff --git a/lib/api/v3/utilities/path_helper.rb b/lib/api/v3/utilities/path_helper.rb index 94dd782fda..691aac0ff7 100644 --- a/lib/api/v3/utilities/path_helper.rb +++ b/lib/api/v3/utilities/path_helper.rb @@ -143,6 +143,10 @@ module API "#{query(id)}/unstar" end + def self.query_column(name) + "#{queries}/columns/#{name}" + end + def self.relation(id) "#{root}/relations/#{id}" end diff --git a/spec/lib/api/v3/queries/columns/query_column_representer_spec.rb b/spec/lib/api/v3/queries/columns/query_column_representer_spec.rb new file mode 100644 index 0000000000..90825ef728 --- /dev/null +++ b/spec/lib/api/v3/queries/columns/query_column_representer_spec.rb @@ -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 diff --git a/spec/lib/api/v3/queries/query_representer_spec.rb b/spec/lib/api/v3/queries/query_representer_spec.rb index d0cbdcf23a..26190eb8db 100644 --- a/spec/lib/api/v3/queries/query_representer_spec.rb +++ b/spec/lib/api/v3/queries/query_representer_spec.rb @@ -33,7 +33,9 @@ describe ::API::V3::Queries::QueryRepresenter do let(:query) { FactoryGirl.build_stubbed(:query, project: project) } let(:project) { FactoryGirl.build_stubbed(:project) } - let(:representer) { described_class.new(query, current_user: double('current_user')) } + let(:representer) do + described_class.new(query, current_user: double('current_user'), embed_links: true) + end subject { representer.to_json } @@ -141,6 +143,55 @@ describe ::API::V3::Queries::QueryRepresenter do 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 end it 'should show an id' do @@ -209,10 +260,29 @@ describe ::API::V3::Queries::QueryRepresenter do 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 diff --git a/spec/lib/api/v3/utilities/path_helper_spec.rb b/spec/lib/api/v3/utilities/path_helper_spec.rb index e9a354d02f..c711dcefc1 100644 --- a/spec/lib/api/v3/utilities/path_helper_spec.rb +++ b/spec/lib/api/v3/utilities/path_helper_spec.rb @@ -242,6 +242,12 @@ 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 'relations paths' do describe '#relation' do subject { helper.relation 1 } diff --git a/spec/requests/api/v3/queries/columns/query_columns_resource_spec.rb b/spec/requests/api/v3/queries/columns/query_columns_resource_spec.rb new file mode 100644 index 0000000000..dec0b4030e --- /dev/null +++ b/spec/requests/api/v3/queries/columns/query_columns_resource_spec.rb @@ -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 From 9cb9096491d2ff7ed2eadca5e18aef596336f97f Mon Sep 17 00:00:00 2001 From: Jens Ulferts Date: Thu, 26 Jan 2017 17:08:23 +0100 Subject: [PATCH 07/16] link to group_by --- app/models/query.rb | 4 + .../group_bys/query_group_by_representer.rb | 67 ++++++++++++++ .../queries/group_bys/query_group_bys_api.rb | 66 ++++++++++++++ lib/api/v3/queries/queries_api.rb | 1 + lib/api/v3/queries/query_representer.rb | 37 ++++++-- lib/api/v3/utilities/path_helper.rb | 4 + .../query_group_by_representer_spec.rb | 90 ++++++++++++++++++ .../api/v3/queries/query_representer_spec.rb | 67 ++++++++++---- spec/lib/api/v3/utilities/path_helper_spec.rb | 6 ++ .../query_group_bys_resource_spec.rb | 91 +++++++++++++++++++ 10 files changed, 411 insertions(+), 22 deletions(-) create mode 100644 lib/api/v3/queries/group_bys/query_group_by_representer.rb create mode 100644 lib/api/v3/queries/group_bys/query_group_bys_api.rb create mode 100644 spec/lib/api/v3/queries/group_bys/query_group_by_representer_spec.rb create mode 100644 spec/requests/api/v3/queries/group_bys/query_group_bys_resource_spec.rb diff --git a/app/models/query.rb b/app/models/query.rb index 698afc9e29..ccdaf44e44 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -279,6 +279,10 @@ class Query < ActiveRecord::Base .concat(available_columns) end + def self.groupable_columns + all_columns.select(&:groupable) + end + def self.add_available_column(column) available_columns << column if column.is_a?(QueryColumn) end diff --git a/lib/api/v3/queries/group_bys/query_group_by_representer.rb b/lib/api/v3/queries/group_bys/query_group_by_representer.rb new file mode 100644 index 0000000000..f1fb6346e1 --- /dev/null +++ b/lib/api/v3/queries/group_bys/query_group_by_representer.rb @@ -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 diff --git a/lib/api/v3/queries/group_bys/query_group_bys_api.rb b/lib/api/v3/queries/group_bys/query_group_bys_api.rb new file mode 100644 index 0000000000..0b21c7a6e7 --- /dev/null +++ b/lib/api/v3/queries/group_bys/query_group_bys_api.rb @@ -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 diff --git a/lib/api/v3/queries/queries_api.rb b/lib/api/v3/queries/queries_api.rb index f66493b50e..c57c4975ba 100644 --- a/lib/api/v3/queries/queries_api.rb +++ b/lib/api/v3/queries/queries_api.rb @@ -35,6 +35,7 @@ module API class QueriesAPI < ::API::OpenProjectAPI resources :queries do mount API::V3::Queries::Columns::QueryColumnsAPI + mount API::V3::Queries::GroupBys::QueryGroupBysAPI get do authorize_any [:view_work_packages, :manage_public_queries], global: true diff --git a/lib/api/v3/queries/query_representer.rb b/lib/api/v3/queries/query_representer.rb index 8cfdc17438..054c95f592 100644 --- a/lib/api/v3/queries/query_representer.rb +++ b/lib/api/v3/queries/query_representer.rb @@ -76,6 +76,22 @@ module API 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 + linked_property :user linked_property :project @@ -100,12 +116,6 @@ module API [convert_attribute(attribute), order] end } - property :group_by, - exec_context: :decorator, - getter: ->(*) { - represented.grouped? ? convert_attribute(represented.group_by) : nil - }, - render_nil: true property :display_sums, getter: -> (*) { display_sums } property :is_starred, getter: -> (*) { starred } @@ -121,6 +131,21 @@ module API embed_links } + property :group_by, + exec_context: :decorator, + getter: ->(*) { + 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 :results, exec_context: :decorator, render_nil: true, diff --git a/lib/api/v3/utilities/path_helper.rb b/lib/api/v3/utilities/path_helper.rb index 691aac0ff7..52bba939fe 100644 --- a/lib/api/v3/utilities/path_helper.rb +++ b/lib/api/v3/utilities/path_helper.rb @@ -147,6 +147,10 @@ module API "#{queries}/columns/#{name}" end + def self.query_group_by(name) + "#{queries}/group_bys/#{name}" + end + def self.relation(id) "#{root}/relations/#{id}" end diff --git a/spec/lib/api/v3/queries/group_bys/query_group_by_representer_spec.rb b/spec/lib/api/v3/queries/group_bys/query_group_by_representer_spec.rb new file mode 100644 index 0000000000..ab494fe0ea --- /dev/null +++ b/spec/lib/api/v3/queries/group_bys/query_group_by_representer_spec.rb @@ -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 diff --git a/spec/lib/api/v3/queries/query_representer_spec.rb b/spec/lib/api/v3/queries/query_representer_spec.rb index 26190eb8db..702dc2e7d4 100644 --- a/spec/lib/api/v3/queries/query_representer_spec.rb +++ b/spec/lib/api/v3/queries/query_representer_spec.rb @@ -192,6 +192,30 @@ describe ::API::V3::Queries::QueryRepresenter do .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 end it 'should show an id' do @@ -210,22 +234,6 @@ describe ::API::V3::Queries::QueryRepresenter 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 - end - describe 'with filters' do let(:query) do query = FactoryGirl.build_stubbed(:query) @@ -286,6 +294,33 @@ describe ::API::V3::Queries::QueryRepresenter do 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 diff --git a/spec/lib/api/v3/utilities/path_helper_spec.rb b/spec/lib/api/v3/utilities/path_helper_spec.rb index c711dcefc1..9d451a2d46 100644 --- a/spec/lib/api/v3/utilities/path_helper_spec.rb +++ b/spec/lib/api/v3/utilities/path_helper_spec.rb @@ -248,6 +248,12 @@ describe ::API::V3::Utilities::PathHelper do 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 'relations paths' do describe '#relation' do subject { helper.relation 1 } diff --git a/spec/requests/api/v3/queries/group_bys/query_group_bys_resource_spec.rb b/spec/requests/api/v3/queries/group_bys/query_group_bys_resource_spec.rb new file mode 100644 index 0000000000..d49d5651f4 --- /dev/null +++ b/spec/requests/api/v3/queries/group_bys/query_group_bys_resource_spec.rb @@ -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 From 58df267bc43547257b857c439fb4f8d82a7716d5 Mon Sep 17 00:00:00 2001 From: Jens Ulferts Date: Mon, 30 Jan 2017 09:57:25 +0100 Subject: [PATCH 08/16] link to sort_bys --- app/models/query.rb | 4 + config/locales/en.yml | 3 + lib/api/decorators/single.rb | 24 ++-- lib/api/v3/queries/queries_api.rb | 1 + lib/api/v3/queries/query_representer.rb | 33 ++++- .../sort_bys/query_sort_by_representer.rb | 73 ++++++++++ .../v3/queries/sort_bys/query_sort_bys_api.rb | 68 +++++++++ .../v3/queries/sort_bys/sort_by_decorator.rb | 99 +++++++++++++ lib/api/v3/utilities/path_helper.rb | 4 + .../api/v3/queries/query_representer_spec.rb | 44 +++++- .../query_sort_by_representer_spec.rb | 130 ++++++++++++++++++ spec/lib/api/v3/utilities/path_helper_spec.rb | 6 + .../sort_bys/query_sort_bys_resource_spec.rb | 101 ++++++++++++++ 13 files changed, 574 insertions(+), 16 deletions(-) create mode 100644 lib/api/v3/queries/sort_bys/query_sort_by_representer.rb create mode 100644 lib/api/v3/queries/sort_bys/query_sort_bys_api.rb create mode 100644 lib/api/v3/queries/sort_bys/sort_by_decorator.rb create mode 100644 spec/lib/api/v3/queries/sort_bys/query_sort_by_representer_spec.rb create mode 100644 spec/requests/api/v3/queries/sort_bys/query_sort_bys_resource_spec.rb diff --git a/app/models/query.rb b/app/models/query.rb index ccdaf44e44..325b79810d 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -283,6 +283,10 @@ class Query < ActiveRecord::Base 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 diff --git a/config/locales/en.yml b/config/locales/en.yml index 1be25be994..661065025a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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: diff --git a/lib/api/decorators/single.rb b/lib/api/decorators/single.rb index 0e5e3f89a3..0a49383338 100644 --- a/lib/api/decorators/single.rb +++ b/lib/api/decorators/single.rb @@ -65,15 +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 + self_path = self_v3_path(path, id_attribute) - id = if id_attribute.respond_to?(:call) - instance_eval(&id_attribute) - else - represented.send(id_attribute) - end - - link_object = { href: api_v3_paths.send(path, id) } + link_object = { href: self_path } title = instance_eval(&title_getter) link_object[:title] = title if title @@ -158,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 diff --git a/lib/api/v3/queries/queries_api.rb b/lib/api/v3/queries/queries_api.rb index c57c4975ba..f2abc4d717 100644 --- a/lib/api/v3/queries/queries_api.rb +++ b/lib/api/v3/queries/queries_api.rb @@ -36,6 +36,7 @@ module API resources :queries do mount API::V3::Queries::Columns::QueryColumnsAPI mount API::V3::Queries::GroupBys::QueryGroupBysAPI + mount API::V3::Queries::SortBys::QuerySortBysAPI get do authorize_any [:view_work_packages, :manage_public_queries], global: true diff --git a/lib/api/v3/queries/query_representer.rb b/lib/api/v3/queries/query_representer.rb index 054c95f592..97051c2cee 100644 --- a/lib/api/v3/queries/query_representer.rb +++ b/lib/api/v3/queries/query_representer.rb @@ -92,6 +92,15 @@ module API 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 @@ -108,14 +117,21 @@ module API end } property :is_public, getter: -> (*) { is_public } - property :sort_criteria, + + property :sort_by, exec_context: :decorator, getter: ->(*) { - return nil unless represented.sort_criteria - represented.sort_criteria.map do |attribute, order| - [convert_attribute(attribute), order] + 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 :display_sums, getter: -> (*) { display_sums } property :is_starred, getter: -> (*) { starred } @@ -163,6 +179,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 diff --git a/lib/api/v3/queries/sort_bys/query_sort_by_representer.rb b/lib/api/v3/queries/sort_bys/query_sort_by_representer.rb new file mode 100644 index 0000000000..113be90b86 --- /dev/null +++ b/lib/api/v3/queries/sort_bys/query_sort_by_representer.rb @@ -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 diff --git a/lib/api/v3/queries/sort_bys/query_sort_bys_api.rb b/lib/api/v3/queries/sort_bys/query_sort_bys_api.rb new file mode 100644 index 0000000000..c3dde38ad4 --- /dev/null +++ b/lib/api/v3/queries/sort_bys/query_sort_bys_api.rb @@ -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 diff --git a/lib/api/v3/queries/sort_bys/sort_by_decorator.rb b/lib/api/v3/queries/sort_bys/sort_by_decorator.rb new file mode 100644 index 0000000000..debf376992 --- /dev/null +++ b/lib/api/v3/queries/sort_bys/sort_by_decorator.rb @@ -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 diff --git a/lib/api/v3/utilities/path_helper.rb b/lib/api/v3/utilities/path_helper.rb index 52bba939fe..240e53b931 100644 --- a/lib/api/v3/utilities/path_helper.rb +++ b/lib/api/v3/utilities/path_helper.rb @@ -151,6 +151,10 @@ module API "#{queries}/group_bys/#{name}" end + def self.query_sort_by(name, direction) + "#{queries}/sort_bys/#{name}-#{direction}" + end + def self.relation(id) "#{root}/relations/#{id}" end diff --git a/spec/lib/api/v3/queries/query_representer_spec.rb b/spec/lib/api/v3/queries/query_representer_spec.rb index 702dc2e7d4..5d392394eb 100644 --- a/spec/lib/api/v3/queries/query_representer_spec.rb +++ b/spec/lib/api/v3/queries/query_representer_spec.rb @@ -99,7 +99,7 @@ describe ::API::V3::Queries::QueryRepresenter do query = FactoryGirl.build_stubbed(:query, project: project) query.add_filter('subject', '~', ['bogus']) query.group_by = 'author' - query.sort_criteria = [['assigned_to_id', 'asc'], ['type_id', 'desc']] + query.sort_criteria = [['assigned_to', 'asc'], ['type', 'desc']] query end @@ -216,6 +216,38 @@ describe ::API::V3::Queries::QueryRepresenter do 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 it 'should show an id' do @@ -260,10 +292,14 @@ describe ::API::V3::Queries::QueryRepresenter do sort_criteria: [['subject', 'asc'], ['assigned_to', 'desc']]) end - it 'should render the filters' do + 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([['subject', 'asc'], ['assignee', 'desc']].to_json) - .at_path('sortCriteria') + .to be_json_eql('/api/v3/queries/sort_bys/assignee-desc'.to_json) + .at_path('_embedded/sortBy/1/_links/self/href') end end diff --git a/spec/lib/api/v3/queries/sort_bys/query_sort_by_representer_spec.rb b/spec/lib/api/v3/queries/sort_bys/query_sort_by_representer_spec.rb new file mode 100644 index 0000000000..d3d91fb86d --- /dev/null +++ b/spec/lib/api/v3/queries/sort_bys/query_sort_by_representer_spec.rb @@ -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 diff --git a/spec/lib/api/v3/utilities/path_helper_spec.rb b/spec/lib/api/v3/utilities/path_helper_spec.rb index 9d451a2d46..8971fe24ce 100644 --- a/spec/lib/api/v3/utilities/path_helper_spec.rb +++ b/spec/lib/api/v3/utilities/path_helper_spec.rb @@ -254,6 +254,12 @@ describe ::API::V3::Utilities::PathHelper do 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 'relations paths' do describe '#relation' do subject { helper.relation 1 } diff --git a/spec/requests/api/v3/queries/sort_bys/query_sort_bys_resource_spec.rb b/spec/requests/api/v3/queries/sort_bys/query_sort_bys_resource_spec.rb new file mode 100644 index 0000000000..2502d03346 --- /dev/null +++ b/spec/requests/api/v3/queries/sort_bys/query_sort_bys_resource_spec.rb @@ -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 From 27362f785a76678ddf761fdb20d6eff070ba0697 Mon Sep 17 00:00:00 2001 From: Jens Ulferts Date: Mon, 30 Jan 2017 15:47:39 +0100 Subject: [PATCH 09/16] embed filters as objects --- app/models/queries/available_filters.rb | 10 +- app/models/queries/base_filter.rb | 11 ++ app/models/queries/filter_serializer.rb | 1 + .../work_packages/filter/category_filter.rb | 20 +++- .../filter/custom_field_filter.rb | 15 +++ .../work_packages/filter/group_filter.rb | 20 +++- .../filter/principal_base_filter.rb | 10 ++ .../work_packages/filter/priority_filter.rb | 19 ++- .../work_packages/filter/project_filter.rb | 10 ++ .../work_packages/filter/role_filter.rb | 10 ++ .../work_packages/filter/status_filter.rb | 20 +++- .../work_packages/filter/subproject_filter.rb | 20 +++- .../work_packages/filter/type_filter.rb | 10 ++ .../work_packages/filter/version_filter.rb | 10 ++ .../queries/filters/query_filter_decorator.rb | 46 ++++++++ .../query_filter_instance_representer.rb | 96 +++++++++++++++ .../filters/query_filter_representer.rb | 65 ++++++++++ .../v3/queries/filters/query_filters_api.rb | 71 +++++++++++ .../operators/query_operator_representer.rb | 63 ++++++++++ .../queries/operators/query_operators_api.rb | 57 +++++++++ lib/api/v3/queries/queries_api.rb | 2 + lib/api/v3/queries/query_representer.rb | 5 +- lib/api/v3/utilities/path_helper.rb | 8 ++ .../query_filter_instance_representer_spec.rb | 111 ++++++++++++++++++ .../filters/query_filter_representer_spec.rb | 106 +++++++++++++++++ .../query_operator_representer_spec.rb | 66 +++++++++++ .../api/v3/queries/query_representer_spec.rb | 61 ++++++++-- spec/lib/api/v3/utilities/path_helper_spec.rb | 12 ++ spec/models/queries/available_filters_spec.rb | 6 +- .../filter/category_filter_spec.rb | 25 ++++ .../filter/created_at_filter_spec.rb | 2 + .../filter/custom_field_filter_spec.rb | 104 ++++++++++++++++ .../filter/due_date_filter_spec.rb | 2 + .../filter/estimated_hours_filter_spec.rb | 2 + .../work_packages/filter/group_filter_spec.rb | 24 ++++ .../filter/priority_filter_spec.rb | 24 ++++ .../filter/project_filter_spec.rb | 25 ++++ .../filter/responsible_filter_spec.rb | 26 ++++ .../work_packages/filter/role_filter_spec.rb | 24 ++++ .../filter/start_date_filter_spec.rb | 2 + .../filter/status_filter_spec.rb | 23 ++++ .../filter/subject_filter_spec.rb | 3 +- .../filter/subproject_filter_spec.rb | 27 ++++- .../work_packages/filter/type_filter_spec.rb | 25 ++++ .../filter/updated_at_filter_spec.rb | 2 + .../filter/version_filter_spec.rb | 25 ++++ .../filter/watcher_filter_spec.rb | 25 ++++ .../filters/query_filters_resource_spec.rb | 98 ++++++++++++++++ .../query_operators_resource_spec.rb | 82 +++++++++++++ .../queries/filters/shared_filter_examples.rb | 16 +++ 50 files changed, 1511 insertions(+), 36 deletions(-) create mode 100644 lib/api/v3/queries/filters/query_filter_decorator.rb create mode 100644 lib/api/v3/queries/filters/query_filter_instance_representer.rb create mode 100644 lib/api/v3/queries/filters/query_filter_representer.rb create mode 100644 lib/api/v3/queries/filters/query_filters_api.rb create mode 100644 lib/api/v3/queries/operators/query_operator_representer.rb create mode 100644 lib/api/v3/queries/operators/query_operators_api.rb create mode 100644 spec/lib/api/v3/queries/filters/query_filter_instance_representer_spec.rb create mode 100644 spec/lib/api/v3/queries/filters/query_filter_representer_spec.rb create mode 100644 spec/lib/api/v3/queries/operators/query_operator_representer_spec.rb create mode 100644 spec/requests/api/v3/queries/filters/query_filters_resource_spec.rb create mode 100644 spec/requests/api/v3/queries/operators/query_operators_resource_spec.rb diff --git a/app/models/queries/available_filters.rb b/app/models/queries/available_filters.rb index 9d0075c934..17e759fd2f 100644 --- a/app/models/queries/available_filters.rb +++ b/app/models/queries/available_filters.rb @@ -36,6 +36,12 @@ module Queries::AvailableFilters 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 @@ -81,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) diff --git a/app/models/queries/base_filter.rb b/app/models/queries/base_filter.rb index 80212e0ce8..63ed586e15 100644 --- a/app/models/queries/base_filter.rb +++ b/app/models/queries/base_filter.rb @@ -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 diff --git a/app/models/queries/filter_serializer.rb b/app/models/queries/filter_serializer.rb index bd7057bdf7..a40cac1cc0 100644 --- a/app/models/queries/filter_serializer.rb +++ b/app/models/queries/filter_serializer.rb @@ -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? diff --git a/app/models/queries/work_packages/filter/category_filter.rb b/app/models/queries/work_packages/filter/category_filter.rb index 96a58abd31..6b68f2ca66 100644 --- a/app/models/queries/work_packages/filter/category_filter.rb +++ b/app/models/queries/work_packages/filter/category_filter.rb @@ -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 diff --git a/app/models/queries/work_packages/filter/custom_field_filter.rb b/app/models/queries/work_packages/filter/custom_field_filter.rb index 62f6649092..fc3de57e1b 100644 --- a/app/models/queries/work_packages/filter/custom_field_filter.rb +++ b/app/models/queries/work_packages/filter/custom_field_filter.rb @@ -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 diff --git a/app/models/queries/work_packages/filter/group_filter.rb b/app/models/queries/work_packages/filter/group_filter.rb index 7b93783690..62254ec12a 100644 --- a/app/models/queries/work_packages/filter/group_filter.rb +++ b/app/models/queries/work_packages/filter/group_filter.rb @@ -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 diff --git a/app/models/queries/work_packages/filter/principal_base_filter.rb b/app/models/queries/work_packages/filter/principal_base_filter.rb index b52b5352cb..d3ee29f6b1 100644 --- a/app/models/queries/work_packages/filter/principal_base_filter.rb +++ b/app/models/queries/work_packages/filter/principal_base_filter.rb @@ -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 diff --git a/app/models/queries/work_packages/filter/priority_filter.rb b/app/models/queries/work_packages/filter/priority_filter.rb index 8f37bb1961..1a195f10d0 100644 --- a/app/models/queries/work_packages/filter/priority_filter.rb +++ b/app/models/queries/work_packages/filter/priority_filter.rb @@ -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 diff --git a/app/models/queries/work_packages/filter/project_filter.rb b/app/models/queries/work_packages/filter/project_filter.rb index 0a50ae16a7..bd71e0ff42 100644 --- a/app/models/queries/work_packages/filter/project_filter.rb +++ b/app/models/queries/work_packages/filter/project_filter.rb @@ -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 diff --git a/app/models/queries/work_packages/filter/role_filter.rb b/app/models/queries/work_packages/filter/role_filter.rb index 98c0f34773..2d8b2e69d5 100644 --- a/app/models/queries/work_packages/filter/role_filter.rb +++ b/app/models/queries/work_packages/filter/role_filter.rb @@ -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 diff --git a/app/models/queries/work_packages/filter/status_filter.rb b/app/models/queries/work_packages/filter/status_filter.rb index 6a755f899b..29346554bf 100644 --- a/app/models/queries/work_packages/filter/status_filter.rb +++ b/app/models/queries/work_packages/filter/status_filter.rb @@ -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 diff --git a/app/models/queries/work_packages/filter/subproject_filter.rb b/app/models/queries/work_packages/filter/subproject_filter.rb index ef46a2e167..23c3e79f97 100644 --- a/app/models/queries/work_packages/filter/subproject_filter.rb +++ b/app/models/queries/work_packages/filter/subproject_filter.rb @@ -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 diff --git a/app/models/queries/work_packages/filter/type_filter.rb b/app/models/queries/work_packages/filter/type_filter.rb index 478a17bdc9..e881f32a5e 100644 --- a/app/models/queries/work_packages/filter/type_filter.rb +++ b/app/models/queries/work_packages/filter/type_filter.rb @@ -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 diff --git a/app/models/queries/work_packages/filter/version_filter.rb b/app/models/queries/work_packages/filter/version_filter.rb index 52e9d3f441..72fc63e84b 100644 --- a/app/models/queries/work_packages/filter/version_filter.rb +++ b/app/models/queries/work_packages/filter/version_filter.rb @@ -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 diff --git a/lib/api/v3/queries/filters/query_filter_decorator.rb b/lib/api/v3/queries/filters/query_filter_decorator.rb new file mode 100644 index 0000000000..046cad5917 --- /dev/null +++ b/lib/api/v3/queries/filters/query_filter_decorator.rb @@ -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 diff --git a/lib/api/v3/queries/filters/query_filter_instance_representer.rb b/lib/api/v3/queries/filters/query_filter_instance_representer.rb new file mode 100644 index 0000000000..700c95347b --- /dev/null +++ b/lib/api/v3/queries/filters/query_filter_instance_representer.rb @@ -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 diff --git a/lib/api/v3/queries/filters/query_filter_representer.rb b/lib/api/v3/queries/filters/query_filter_representer.rb new file mode 100644 index 0000000000..ca83a72643 --- /dev/null +++ b/lib/api/v3/queries/filters/query_filter_representer.rb @@ -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 diff --git a/lib/api/v3/queries/filters/query_filters_api.rb b/lib/api/v3/queries/filters/query_filters_api.rb new file mode 100644 index 0000000000..bd3710fb16 --- /dev/null +++ b/lib/api/v3/queries/filters/query_filters_api.rb @@ -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 diff --git a/lib/api/v3/queries/operators/query_operator_representer.rb b/lib/api/v3/queries/operators/query_operator_representer.rb new file mode 100644 index 0000000000..a4962fe46d --- /dev/null +++ b/lib/api/v3/queries/operators/query_operator_representer.rb @@ -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 diff --git a/lib/api/v3/queries/operators/query_operators_api.rb b/lib/api/v3/queries/operators/query_operators_api.rb new file mode 100644 index 0000000000..f17e17da6b --- /dev/null +++ b/lib/api/v3/queries/operators/query_operators_api.rb @@ -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 diff --git a/lib/api/v3/queries/queries_api.rb b/lib/api/v3/queries/queries_api.rb index f2abc4d717..b7fa21fce3 100644 --- a/lib/api/v3/queries/queries_api.rb +++ b/lib/api/v3/queries/queries_api.rb @@ -37,6 +37,8 @@ module API 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 diff --git a/lib/api/v3/queries/query_representer.rb b/lib/api/v3/queries/query_representer.rb index 97051c2cee..6fcc06730c 100644 --- a/lib/api/v3/queries/query_representer.rb +++ b/lib/api/v3/queries/query_representer.rb @@ -110,10 +110,7 @@ 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 } diff --git a/lib/api/v3/utilities/path_helper.rb b/lib/api/v3/utilities/path_helper.rb index 240e53b931..f7c6f10552 100644 --- a/lib/api/v3/utilities/path_helper.rb +++ b/lib/api/v3/utilities/path_helper.rb @@ -155,6 +155,14 @@ module API "#{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 diff --git a/spec/lib/api/v3/queries/filters/query_filter_instance_representer_spec.rb b/spec/lib/api/v3/queries/filters/query_filter_instance_representer_spec.rb new file mode 100644 index 0000000000..c252a96a60 --- /dev/null +++ b/spec/lib/api/v3/queries/filters/query_filter_instance_representer_spec.rb @@ -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 diff --git a/spec/lib/api/v3/queries/filters/query_filter_representer_spec.rb b/spec/lib/api/v3/queries/filters/query_filter_representer_spec.rb new file mode 100644 index 0000000000..2799545046 --- /dev/null +++ b/spec/lib/api/v3/queries/filters/query_filter_representer_spec.rb @@ -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 diff --git a/spec/lib/api/v3/queries/operators/query_operator_representer_spec.rb b/spec/lib/api/v3/queries/operators/query_operator_representer_spec.rb new file mode 100644 index 0000000000..c99383f1ee --- /dev/null +++ b/spec/lib/api/v3/queries/operators/query_operator_representer_spec.rb @@ -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 diff --git a/spec/lib/api/v3/queries/query_representer_spec.rb b/spec/lib/api/v3/queries/query_representer_spec.rb index 5d392394eb..8de82e19c0 100644 --- a/spec/lib/api/v3/queries/query_representer_spec.rb +++ b/spec/lib/api/v3/queries/query_representer_spec.rb @@ -269,19 +269,64 @@ describe ::API::V3::Queries::QueryRepresenter do 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 diff --git a/spec/lib/api/v3/utilities/path_helper_spec.rb b/spec/lib/api/v3/utilities/path_helper_spec.rb index 8971fe24ce..ccc1065407 100644 --- a/spec/lib/api/v3/utilities/path_helper_spec.rb +++ b/spec/lib/api/v3/utilities/path_helper_spec.rb @@ -260,6 +260,18 @@ describe ::API::V3::Utilities::PathHelper do 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 } diff --git a/spec/models/queries/available_filters_spec.rb b/spec/models/queries/available_filters_spec.rb index 370254fda8..8b387978a9 100644 --- a/spec/models/queries/available_filters_spec.rb +++ b/spec/models/queries/available_filters_spec.rb @@ -45,9 +45,9 @@ describe Queries::AvailableFilters, type: :model do let(:includer) do includer = HelperClass.new(context) - allow(includer) - .to receive(:registered_filters) - .and_return(registered_filters) + allow(Queries::Register) + .to receive(:filters) + .and_return(HelperClass => registered_filters) includer end diff --git a/spec/models/queries/work_packages/filter/category_filter_spec.rb b/spec/models/queries/work_packages/filter/category_filter_spec.rb index 1cd4bf7310..29647f3299 100644 --- a/spec/models/queries/work_packages/filter/category_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/category_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/created_at_filter_spec.rb b/spec/models/queries/work_packages/filter/created_at_filter_spec.rb index 0e73f1a2f4..dcbf0f26c9 100644 --- a/spec/models/queries/work_packages/filter/created_at_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/created_at_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/custom_field_filter_spec.rb b/spec/models/queries/work_packages/filter/custom_field_filter_spec.rb index ddad568f9e..296e28c002 100644 --- a/spec/models/queries/work_packages/filter/custom_field_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/custom_field_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/due_date_filter_spec.rb b/spec/models/queries/work_packages/filter/due_date_filter_spec.rb index 507b4e54a5..0be7ca7783 100644 --- a/spec/models/queries/work_packages/filter/due_date_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/due_date_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/estimated_hours_filter_spec.rb b/spec/models/queries/work_packages/filter/estimated_hours_filter_spec.rb index 51af5fd84b..b88523db02 100644 --- a/spec/models/queries/work_packages/filter/estimated_hours_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/estimated_hours_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/group_filter_spec.rb b/spec/models/queries/work_packages/filter/group_filter_spec.rb index f08db01929..fd139596f0 100644 --- a/spec/models/queries/work_packages/filter/group_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/group_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/priority_filter_spec.rb b/spec/models/queries/work_packages/filter/priority_filter_spec.rb index a0c475484d..3042e0e5aa 100644 --- a/spec/models/queries/work_packages/filter/priority_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/priority_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/project_filter_spec.rb b/spec/models/queries/work_packages/filter/project_filter_spec.rb index 437cb67cb4..7bd5f3e472 100644 --- a/spec/models/queries/work_packages/filter/project_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/project_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/responsible_filter_spec.rb b/spec/models/queries/work_packages/filter/responsible_filter_spec.rb index c1f09a39cc..fb1471a8c1 100644 --- a/spec/models/queries/work_packages/filter/responsible_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/responsible_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/role_filter_spec.rb b/spec/models/queries/work_packages/filter/role_filter_spec.rb index 84f5b50944..6a7a4843de 100644 --- a/spec/models/queries/work_packages/filter/role_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/role_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/start_date_filter_spec.rb b/spec/models/queries/work_packages/filter/start_date_filter_spec.rb index ae0a89f6b3..c4b765adb0 100644 --- a/spec/models/queries/work_packages/filter/start_date_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/start_date_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/status_filter_spec.rb b/spec/models/queries/work_packages/filter/status_filter_spec.rb index fd29fab16f..65ca6bc41e 100644 --- a/spec/models/queries/work_packages/filter/status_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/status_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/subject_filter_spec.rb b/spec/models/queries/work_packages/filter/subject_filter_spec.rb index 75ad5f012f..7afa4313a8 100644 --- a/spec/models/queries/work_packages/filter/subject_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/subject_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/subproject_filter_spec.rb b/spec/models/queries/work_packages/filter/subproject_filter_spec.rb index 8ecdaf5aed..502d1e7a05 100644 --- a/spec/models/queries/work_packages/filter/subproject_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/subproject_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/type_filter_spec.rb b/spec/models/queries/work_packages/filter/type_filter_spec.rb index 4ec5ebfc12..b5eedb1610 100644 --- a/spec/models/queries/work_packages/filter/type_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/type_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/updated_at_filter_spec.rb b/spec/models/queries/work_packages/filter/updated_at_filter_spec.rb index 7b862ce69c..3f751798bf 100644 --- a/spec/models/queries/work_packages/filter/updated_at_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/updated_at_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/version_filter_spec.rb b/spec/models/queries/work_packages/filter/version_filter_spec.rb index d80acd316e..0a08834d2f 100644 --- a/spec/models/queries/work_packages/filter/version_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/version_filter_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/watcher_filter_spec.rb b/spec/models/queries/work_packages/filter/watcher_filter_spec.rb index 33f18a2feb..6ae547d27b 100644 --- a/spec/models/queries/work_packages/filter/watcher_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/watcher_filter_spec.rb @@ -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 diff --git a/spec/requests/api/v3/queries/filters/query_filters_resource_spec.rb b/spec/requests/api/v3/queries/filters/query_filters_resource_spec.rb new file mode 100644 index 0000000000..0fc4416c74 --- /dev/null +++ b/spec/requests/api/v3/queries/filters/query_filters_resource_spec.rb @@ -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 diff --git a/spec/requests/api/v3/queries/operators/query_operators_resource_spec.rb b/spec/requests/api/v3/queries/operators/query_operators_resource_spec.rb new file mode 100644 index 0000000000..3b1ecb4684 --- /dev/null +++ b/spec/requests/api/v3/queries/operators/query_operators_resource_spec.rb @@ -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 diff --git a/spec/support/queries/filters/shared_filter_examples.rb b/spec/support/queries/filters/shared_filter_examples.rb index e34f1f0c34..de830a1b1e 100644 --- a/spec/support/queries/filters/shared_filter_examples.rb +++ b/spec/support/queries/filters/shared_filter_examples.rb @@ -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 From bf592839930516cb9b20d7d60700e3f265c755e2 Mon Sep 17 00:00:00 2001 From: Jens Ulferts Date: Tue, 31 Jan 2017 14:12:53 +0100 Subject: [PATCH 10/16] rename query properties --- lib/api/v3/queries/query_representer.rb | 6 ++--- .../api/v3/queries/query_representer_spec.rb | 4 ++-- .../api/v3/queries/query_resource_spec.rb | 24 +++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/api/v3/queries/query_representer.rb b/lib/api/v3/queries/query_representer.rb index 6fcc06730c..871c328b53 100644 --- a/lib/api/v3/queries/query_representer.rb +++ b/lib/api/v3/queries/query_representer.rb @@ -113,7 +113,7 @@ module API ::API::V3::Queries::Filters::QueryFilterInstanceRepresenter.new(filter) end } - property :is_public, getter: -> (*) { is_public } + property :public, getter: -> (*) { is_public } property :sort_by, exec_context: :decorator, @@ -129,8 +129,8 @@ module API embed_links } - property :display_sums, getter: -> (*) { display_sums } - property :is_starred, getter: -> (*) { starred } + property :sums, getter: -> (*) { display_sums } + property :starred, getter: -> (*) { starred } property :columns, exec_context: :decorator, diff --git a/spec/lib/api/v3/queries/query_representer_spec.rb b/spec/lib/api/v3/queries/query_representer_spec.rb index 8de82e19c0..f796592837 100644 --- a/spec/lib/api/v3/queries/query_representer_spec.rb +++ b/spec/lib/api/v3/queries/query_representer_spec.rb @@ -259,11 +259,11 @@ 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') + is_expected.to be_json_eql(query.is_public.to_json).at_path('public') end describe 'with filters' do diff --git a/spec/requests/api/v3/queries/query_resource_spec.rb b/spec/requests/api/v3/queries/query_resource_spec.rb index 095f53b1d3..a937a9d20d 100644 --- a/spec/requests/api/v3/queries/query_resource_spec.rb +++ b/spec/requests/api/v3/queries/query_resource_spec.rb @@ -227,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 @@ -237,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 @@ -269,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 @@ -310,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 @@ -324,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 @@ -365,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 From 79e246ecae0cebf3017d361708aa86157145cc1c Mon Sep 17 00:00:00 2001 From: Jens Ulferts Date: Tue, 31 Jan 2017 15:56:32 +0100 Subject: [PATCH 11/16] fix documentation styling --- doc/apiv3/endpoints/work-packages.apib | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/apiv3/endpoints/work-packages.apib b/doc/apiv3/endpoints/work-packages.apib index b60562ba04..d34b0586ef 100644 --- a/doc/apiv3/endpoints/work-packages.apib +++ b/doc/apiv3/endpoints/work-packages.apib @@ -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) From 0e8bea24b7a05631a9ee97409f3ad90bf69a94fa Mon Sep 17 00:00:00 2001 From: Jens Ulferts Date: Tue, 31 Jan 2017 15:56:51 +0100 Subject: [PATCH 12/16] document changes to the query api --- doc/apiv3/endpoints/queries.apib | 1088 +++++++++++++++++++++++++----- 1 file changed, 915 insertions(+), 173 deletions(-) diff --git a/doc/apiv3/endpoints/queries.apib b/doc/apiv3/endpoints/queries.apib index fd895713be..1566d4fc73 100644 --- a/doc/apiv3/endpoints/queries.apib +++ b/doc/apiv3/endpoints/queries.apib @@ -1,91 +1,247 @@ # Group Queries -| Link | Description | Type | Constraints | Supported operations | Condition | -| :----------------: | ------------------------------------------------------------------------ | ------------ | ------------ | --------------------- | ----------------------------------------- | -| self | This query | Query | not null | READ | | -| user | The user that owns this query | User | not null | READ | | -| project | The project on which this query operates | Project | | READ | | +A query defines how work packages can be filtered and displayed. Clients can define a query once, store it, and use it later on to load the same set of filters. + +## Actions + +| Link | Description | Condition | +|:-------------------:|----------------------------------------------------------------------| ---------------------------------------| + +As of now, no actions are defined. + +## Linked Properties + +| Property | Description | Type | Constraints | Supported operations | +| :--------------: | ------------------------------------------------------ | ----------- | -------------------------------- | -------------------- | +| self | This query | Query | not null | READ | +| user | The user that owns this query | User | not null | READ | +| project | The project on which this query operates | Project | | READ | +| columns | Ordered list of QueryColumns. The columns, when maped to WorkPackage properties determine which WorkPackage properties to display | []QueryColumn | | READ | +| sortBy | Ordered list of QuerySortBys. Indicates the WorkPackage property the results will be ordered by as well as the direction | []QuerySortBy | | READ | +| groupBy | The WorkPackage property results of this query are grouped by | String | | READ | +| results | The list of work packages returned by applying the filters, sorting and grouping defined in the query | WorkPackageCollection | | READ | + +Please note, that all the properties listed above will also be embedded when individual queries are returned but will not be embedded when a list of queries is returned. Whether the properties are embedded or not may be subject to change in the future. ## Local Properties -| Property | Description | Type | Constraints | Supported operations | -| :--------------: | ------------------------------------------------------ | ----------- | -------------------------------- | -------------------- | -| id | Query id | Integer | x > 0 | READ | -| name | Query name | String | | READ | -| filters | An object describing the queries filter conditions | Object | | READ | -| columnNames | Ordered list of properties to be shown in this query | String[] | | READ | -| sortCriteria | An object describing the sorting rules of this query | Object | | READ | -| groupBy | The property to group results of this query by | String | | READ | -| displaySums | Should sums (of supported properties) be shown? | Boolean | | READ | -| isPublic | Can users besides the owner see the query? | Boolean | | READ | -| isStarred | Should the query be highlighted to the user? | Boolean | | READ | +| Property | Description | Type | Constraints | Supported operations | +| :--------------: | ------------------------------------------------------ | ----------- | -------------------------------- | -------------------- | +| id | Query id | Integer | x > 0 | READ | +| name | Query name | String | | READ | +| filters | A set of QueryFilters which will be applied to the work packages to determine the resulting work packages | []QueryFilterInstance | | READ | +| sums | Should sums (of supported properties) be shown? | Boolean | | READ | +| public | Can users besides the owner see the query? | Boolean | | READ | +| starred | Should the query be highlighted to the user? | Boolean | | READ | A query that is not assigned to a project (`"project": null`) is called a global query. Global queries filter work packages regarless of the project they are assigned to. As such, a different set of filters exists for those queries. -## Query [/api/v3/queries/{id}] +## Query Filter Instance + +A QueryFilterInstance defines a filtering applied to the list of work packages. As such it contains of: +* the filter type (`QueryFilter`) used +* the operator (`QueryOperator`) used +* the list of values + +The list of values can either consist of a list of links or of a list of strings. If the values are primitive (e.g. Integer, Boolean, Date) they will be displayed as strings and the QueryFilterInstance will have a `values` property. + +``` + { + "_type": "DueDateQueryFilter", + "name": "Due date", + "values": [ + "1" + ], + "_links": { + "filter": { + "href": "/api/v3/queries/filters/dueDate", + "title": "Due date" + }, + "operator": { + "href": "/api/v3/queries/operators/" ] + }, + "_links": { + "self": { + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=1&pageSize=2&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D" + }, + "jumpTo": { + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=%7Boffset%7D&pageSize=2&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D", + "templated": true + }, + "changeSize": { + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=1&pageSize=%7Bsize%7D&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D", + "templated": true + }, + "createWorkPackage": { + "href": "/api/v3/work_packages/form", + "method": "post" + }, + "createWorkPackageImmediate": { + "href": "/api/v3/work_packages", + "method": "post" + } + } + } + }, + "_links": { + "self": { + "href": "/api/v3/queries/9", + "title": "fdsfdsfdsf" + }, + "results": { + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=1&pageSize=2&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D" + }, + "columns": [ + { + "href": "/api/v3/queries/columns/id", + "title": "ID" + }, + { + "href": "/api/v3/queries/columns/subject", + "title": "Subject" + }, + { + "href": "/api/v3/queries/columns/type", + "title": "Type" + }, + { + "href": "/api/v3/queries/columns/status", + "title": "Status" + }, + { + "href": "/api/v3/queries/columns/priority", + "title": "Priority" + }, + { + "href": "/api/v3/queries/columns/assignee", + "title": "Assignee" + }, + { + "href": "/api/v3/queries/columns/updated_at", + "title": "Updated on" + } + ], + "groupBy": { + "href": null, + "title": null + }, + "sortBy": [ + { + "href": "/api/v3/queries/sort_bys/parent-desc", + "title": "Parent (Descending)" + } ], - "groupBy": null, - "displaySums": false, - "isStarred": true + "user": { + "href": "/api/v3/users/1", + "title": "OpenProject Admin" + }, + "project": { + "href": "/api/v3/projects/3", + "title": "copy" + } + } } ## View query [GET] -*might be subject to change in the future* +Retreive an individual query as identified by the id parameter. Then end point accepts a number of parameters that can be used to override the resources' persisted parameters. + Parameters + id (required, integer, `1`) ... Query id + + filters (optional, string, `[{ "assignee": { "operator": "=", "values": ["1", "5"] }" }]`) ... JSON specifying filter conditions. The filters provided as parameters are not applied to the query but are instead used to override the query's persisted filters. All filters also accepted by the work packages endpoint are accepted. + + offset = `1` (optional, integer, `25`) ... Page number inside the queries' result collection of work packages. + + pageSize (optional, integer, `25`) ... Number of elements to display per page for the queries' result collection of work packages. + + sortBy (optional, string, `[["status", "asc"]]`) ... JSON specifying sort criteria. The sort criteria is applied to the querie's result collection of work packages overriding the query's persisted sort criteria. + + groupBy (optional, string, `status`) ... The column to group by. The grouping criteria is applied to the to the querie's result collection of work packages overriding the query's persisted group criteria. + + showSums = `false` (optional, boolean, `true`) ... Indicates whether properties should be summed up if they support it. The showSums parameter is applied to the to the querie's result collection of work packages overriding the query's persisted sums property. + Response 200 (application/hal+json) @@ -109,6 +265,8 @@ A query that is not assigned to a project (`"project": null`) is called a global ## Delete query [DELETE] +Delete the query identified by the id parameter + + Parameters + id (required, integer, `1`) ... Query id @@ -152,56 +310,138 @@ A query that is not assigned to a project (`"project": null`) is called a global + Body { - "_type": "Query", - "_links": { - "self": { - "href": "/api/v3/queries/2", - "title": "My work packages" + "_type": "Query", + "id": 9, + "name": "fdsfdsfdsf", + "filters": [ + { + "_type": "StatusQueryFilter", + "name": "Status", + "_links": { + "filter": { + "href": "/api/v3/queries/filters/status", + "title": "Status" }, - "project": { - "href": "/api/v3/projects/1", - "title": "Lorem ipsum" + "operator": { + "href": "/api/v3/queries/operators/o", + "title": "open" }, - "user": { - "href": "/api/v3/users/1", - "title": "John Sheppard - admin" - } + "values": [] + } }, - "id": 2, - "name": "My work packages", - "filters": [ - { - "status": { - "operator": "o", - "values": null - } + { + "_type": "DueDateQueryFilter", + "name": "Due date", + "values": [ + "1" + ], + "_links": { + "filter": { + "href": "/api/v3/queries/filters/dueDate", + "title": "Due date" }, - { - "assignee": { - "operator": "=", - "values": [ - "me" - ] - } + "operator": { + "href": "/api/v3/queries/operators/" ] + }, + "_links": { + "self": { + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=1&pageSize=2&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D" + }, + "jumpTo": { + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=%7Boffset%7D&pageSize=2&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D", + "templated": true + }, + "changeSize": { + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=1&pageSize=%7Bsize%7D&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D", + "templated": true + }, + "createWorkPackage": { + "href": "/api/v3/work_packages/form", + "method": "post" + }, + "createWorkPackageImmediate": { + "href": "/api/v3/work_packages", + "method": "post" + } + } + } + }, + "_links": { + "self": { + "href": "/api/v3/queries/9", + "title": "fdsfdsfdsf" + }, + "results": { + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=1&pageSize=2&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D" + }, + "columns": [ + { + "href": "/api/v3/queries/columns/id", + "title": "ID" + }, + { + "href": "/api/v3/queries/columns/subject", + "title": "Subject" + }, + { + "href": "/api/v3/queries/columns/type", + "title": "Type" + }, + { + "href": "/api/v3/queries/columns/status", + "title": "Status" + }, + { + "href": "/api/v3/queries/columns/priority", + "title": "Priority" + }, + { + "href": "/api/v3/queries/columns/assignee", + "title": "Assignee" + }, + { + "href": "/api/v3/queries/columns/updated_at", + "title": "Updated on" + } ], - "groupBy": null, - "displaySums": false, - "isStarred": true + "groupBy": { + "href": null, + "title": null + }, + "sortBy": [ + { + "href": "/api/v3/queries/sort_bys/parent-desc", + "title": "Parent (Descending)" + } + ], + "user": { + "href": "/api/v3/users/1", + "title": "OpenProject Admin" + }, + "project": { + "href": "/api/v3/projects/3", + "title": "copy" + } + } } ## Star query [PATCH] @@ -263,56 +503,138 @@ A query that is not assigned to a project (`"project": null`) is called a global + Body { - "_type": "Query", - "_links": { - "self": { - "href": "/api/v3/queries/2", - "title": "My work packages" + "_type": "Query", + "id": 9, + "name": "fdsfdsfdsf", + "filters": [ + { + "_type": "StatusQueryFilter", + "name": "Status", + "_links": { + "filter": { + "href": "/api/v3/queries/filters/status", + "title": "Status" }, - "project": { - "href": "/api/v3/projects/1", - "title": "Lorem ipsum" + "operator": { + "href": "/api/v3/queries/operators/o", + "title": "open" }, - "user": { - "href": "/api/v3/users/1", - "title": "John Sheppard - admin" - } + "values": [] + } }, - "id": 2, - "name": "My work packages", - "filters": [ - { - "status": { - "operator": "o", - "values": null - } + { + "_type": "DueDateQueryFilter", + "name": "Due date", + "values": [ + "1" + ], + "_links": { + "filter": { + "href": "/api/v3/queries/filters/dueDate", + "title": "Due date" }, - { - "assignee": { - "operator": "=", - "values": [ - "me" - ] - } + "operator": { + "href": "/api/v3/queries/operators/" ] + }, + "_links": { + "self": { + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=1&pageSize=2&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D" + }, + "jumpTo": { + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=%7Boffset%7D&pageSize=2&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D", + "templated": true + }, + "changeSize": { + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=1&pageSize=%7Bsize%7D&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D", + "templated": true + }, + "createWorkPackage": { + "href": "/api/v3/work_packages/form", + "method": "post" + }, + "createWorkPackageImmediate": { + "href": "/api/v3/work_packages", + "method": "post" + } + } + } + }, + "_links": { + "self": { + "href": "/api/v3/queries/9", + "title": "fdsfdsfdsf" + }, + "results": { + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=1&pageSize=2&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D" + }, + "columns": [ + { + "href": "/api/v3/queries/columns/id", + "title": "ID" + }, + { + "href": "/api/v3/queries/columns/subject", + "title": "Subject" + }, + { + "href": "/api/v3/queries/columns/type", + "title": "Type" + }, + { + "href": "/api/v3/queries/columns/status", + "title": "Status" + }, + { + "href": "/api/v3/queries/columns/priority", + "title": "Priority" + }, + { + "href": "/api/v3/queries/columns/assignee", + "title": "Assignee" + }, + { + "href": "/api/v3/queries/columns/updated_at", + "title": "Updated on" + } + ], + "groupBy": { + "href": null, + "title": null + }, + "sortBy": [ + { + "href": "/api/v3/queries/sort_bys/parent-desc", + "title": "Parent (Descending)" + } ], - "groupBy": null, - "displaySums": false, - "isStarred": false + "user": { + "href": "/api/v3/users/1", + "title": "OpenProject Admin" + }, + "project": { + "href": "/api/v3/projects/3", + "title": "copy" + } + } } ## Unstar query [PATCH] @@ -388,38 +710,138 @@ A query that is not assigned to a project (`"project": null`) is called a global { "elements": [ { - "_type": "Query", - "_links": { + "_type": "Query", + "id": 9, + "name": "fdsfdsfdsf", + "filters": [ + { + "_type": "StatusQueryFilter", + "name": "Status", + "_links": { + "filter": { + "href": "/api/v3/queries/filters/status", + "title": "Status" + }, + "operator": { + "href": "/api/v3/queries/operators/o", + "title": "open" + }, + "values": [] + } + }, + { + "_type": "DueDateQueryFilter", + "name": "Due date", + "values": [ + "1" + ], + "_links": { + "filter": { + "href": "/api/v3/queries/filters/dueDate", + "title": "Due date" + }, + "operator": { + "href": "/api/v3/queries/operators/" + ] + }, + "_links": { "self": { - "href": "/api/v3/queries/2", - "title": "My work packages" + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=1&pageSize=2&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D" + }, + "jumpTo": { + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=%7Boffset%7D&pageSize=2&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D", + "templated": true }, - "project": { - "href": null + "changeSize": { + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=1&pageSize=%7Bsize%7D&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D", + "templated": true }, - "user": { - "href": "/api/v3/users/1", - "title": "John Sheppard" + "createWorkPackage": { + "href": "/api/v3/work_packages/form", + "method": "post" + }, + "createWorkPackageImmediate": { + "href": "/api/v3/work_packages", + "method": "post" } + } + } + }, + "_links": { + "self": { + "href": "/api/v3/queries/9", + "title": "fdsfdsfdsf" + }, + "results": { + "href": "/api/v3/projects/3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22o%22%2C%22values%22%3A%5B%5D%7D%7D%2C%7B%22dueDate%22%3A%7B%22operator%22%3A%22%3Ct%2B%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D&offset=1&pageSize=2&sortBy=%5B%5B%22parent%22%2C%22desc%22%5D%5D" }, - "id": 2, - "name": "My work packages", - "filters": [], - "isPublic": false, - "columnNames": [ - "type", - "status", - "subject" + "columns": [ + { + "href": "/api/v3/queries/columns/id", + "title": "ID" + }, + { + "href": "/api/v3/queries/columns/subject", + "title": "Subject" + }, + { + "href": "/api/v3/queries/columns/type", + "title": "Type" + }, + { + "href": "/api/v3/queries/columns/status", + "title": "Status" + }, + { + "href": "/api/v3/queries/columns/priority", + "title": "Priority" + }, + { + "href": "/api/v3/queries/columns/assignee", + "title": "Assignee" + }, + { + "href": "/api/v3/queries/columns/updated_at", + "title": "Updated on" + } ], - "sortCriteria": [ - [ - "parent", - "desc" - ] + "groupBy": { + "href": null, + "title": null + }, + "sortBy": [ + { + "href": "/api/v3/queries/sort_bys/parent-desc", + "title": "Parent (Descending)" + } ], - "groupBy": null, - "displaySums": false, - "isStarred": true + "user": { + "href": "/api/v3/users/1", + "title": "OpenProject Admin" + }, + "project": { + "href": "/api/v3/projects/3", + "title": "copy" + } + } } ] } @@ -427,7 +849,7 @@ A query that is not assigned to a project (`"project": null`) is called a global ## List queries [GET] -Returns a collection of queries. The collection can be filtered via query parameters similar to how work packages are filtered. Please note however, that the filters are applied to the queries and not to the work packages the filters in turn might return. +Returns a collection of queries. The collection can be filtered via query parameters similar to how work packages are filtered. Please note however, that the filters are applied to the queries and not to the work packages the queries in turn might return. + Parameters + filters (optional, string, `[{ "project_id": { "operator": "!*", "values": null }" }]`) ... JSON specifying filter conditions. @@ -452,3 +874,323 @@ Returns a collection of queries. The collection can be filtered via query parame "errorIdentifier": "urn:openproject-org:api:v3:errors:MissingPermission", "message": "You are not allowed to see the queries." } + +# Group Query Columns + +A QueryColumn can be referenced by a Query to denote the work package properties the client should display for the work packages returned as query results. The columns maps to the WorkPackage by the id property. + +## Actions + +| Link | Description | Condition | +|:-------------------:|----------------------------------------------------------------------| ---------------------------------------| + +As of now, no actions are defined. + +## Linked Properties + +| Property | Description | Type | Constraints | Supported operations | +| :--------------: | ------------------------------------------------------ | ----------- | -------------------------------- | -------------------- | +| self | This query column | QueryColumn | not null | READ | + +## Local Properties + +| Property | Description | Type | Constraints | Supported operations | +| :--------------: | ------------------------------------------------------ | ----------- | -------------------------------- | -------------------- | +| id | Query column id | String | not null | READ | +| name | Query column name | String | not null | READ | + +## Query Column [/api/v3/queries/columns/{id}] + ++ Model + + Body + + { + "_type": "QueryColumn", + "id": "priority", + "name": "Priority", + "_links": { + "self": { + "href": "/api/v3/queries/columns/priority", + "title": "Priority" + } + } + } + +## View Query Column [GET] + +Retreive an individual QueryColumn as identified by the `id` parameter. + ++ Parameters + + id (required, string, `priority`) ... QueryColumn id + ++ Response 200 (application/hal+json) + + [Query Column][] + ++ Response 403 (application/hal+json) + + Returned if the client does not have sufficient permissions to see it. + + **Required permission:** view work package in any project + + + Body + + { + "_type": "Error", + "errorIdentifier": "urn:openproject-org:api:v3:errors:Unauthenticated", + "message": "You need to be authenticated to access this resource." + } + ++ Response 404 (application/hal+json) + + Returned if the QueryColumn does not exist. + + + Body + + { + "_type": "Error", + "errorIdentifier": "urn:openproject-org:api:v3:errors:NotFound", + "message": "The specified query does not exist." + } + +# Group Query Filters + +A QueryFilter can be referenced by a filter instance defined for a Query to denote the filtering applied to the query's work package results. This resource is not an instance of an applicable filter but rather the type an applicable filter can have. + +## Actions + +| Link | Description | Condition | +|:-------------------:|----------------------------------------------------------------------| ---------------------------------------| + +As of now, no actions are defined. + +## Linked Properties + +| Property | Description | Type | Constraints | Supported operations | +| :--------------: | ------------------------------------------------------ | ----------- | -------------------------------- | -------------------- | +| self | This query filter | QueryFilter | not null | READ | + +## Local Properties + +| Property | Description | Type | Constraints | Supported operations | +| :--------------: | ------------------------------------------------------ | ----------- | -------------------------------- | -------------------- | +| id | QueryFilter id | String | not null | READ | + +## Query Filter [/api/v3/queries/filters/{id}] + ++ Model + + Body + + { + "_type": "QueryFilter", + "id": "status", + "_links": { + "self": { + "href": "/api/v3/queries/filters/status", + "title": "Status" + } + } + } + +## View Query Filter [GET] + +Retreive an individual QueryFilter as identified by the id parameter. + ++ Parameters + + id (required, string, `status`) ... QueryFilter identifier. + ++ Response 200 (application/hal+json) + + [Query Filter][] + ++ Response 403 (application/hal+json) + + Returned if the client does not have sufficient permissions to see it. + + **Required permission:** view work package in any project + + + Body + + { + "_type": "Error", + "errorIdentifier": "urn:openproject-org:api:v3:errors:Unauthenticated", + "message": "You need to be authenticated to access this resource." + } + ++ Response 404 (application/hal+json) + + Returned if the QueryFilter does not exist. + + + Body + + { + "_type": "Error", + "errorIdentifier": "urn:openproject-org:api:v3:errors:NotFound", + "message": "The specified query does not exist." + } + +# Group Query Operators + +A QueryOperator can be referenced by a QueryFilter to denote the operator to be applied to the filter relation. + +## Actions + +| Link | Description | Condition | +|:-------------------:|----------------------------------------------------------------------| ---------------------------------------| + +As of now, no actions are defined. + +## Linked Properties + +| Property | Description | Type | Constraints | Supported operations | +| :--------------: | ------------------------------------------------------ | ----------- | -------------------------------- | -------------------- | +| self | This query operator | QueryOperator | not null | READ | + +## Local Properties + +| Property | Description | Type | Constraints | Supported operations | +| :--------------: | ------------------------------------------------------ | ----------- | -------------------------------- | -------------------- | +| id | Query oprator id | String | not null | READ | +| name | Query operator name | String | not null | READ | + +## Query Operator [/api/v3/queries/operators/{id}] + ++ Model + + Body + + { + "_type": "QueryOperator", + "id": "!", + "name": "is not", + "_links": { + "self": { + "href": "/api/v3/queries/operators/!", + "title": "is not" + } + } + } + +## View Query Operator [GET] + +Retreive an individual QueryOperator as identified by the `id` parameter. + ++ Parameters + + id (required, string, `!`) ... QueryOperator id + ++ Response 200 (application/hal+json) + + [Query Operator][] + ++ Response 403 (application/hal+json) + + Returned if the client does not have sufficient permissions to see it. + + **Required permission:** view work package in any project + + + Body + + { + "_type": "Error", + "errorIdentifier": "urn:openproject-org:api:v3:errors:Unauthenticated", + "message": "You need to be authenticated to access this resource." + } + ++ Response 404 (application/hal+json) + + Returned if the QueryOperator does not exist. + + + Body + + { + "_type": "Error", + "errorIdentifier": "urn:openproject-org:api:v3:errors:NotFound", + "message": "The specified query does not exist." + } + +# Group Query Sort Bys + +A QuerySortBy can be referenced by a Query to denote the sorting applied to the query's work package results. It consists of the columns to sort by as well as the direction (ascending/descending) + +## Actions + +| Link | Description | Condition | +|:-------------------:|----------------------------------------------------------------------| ---------------------------------------| + +As of now, no actions are defined. + +## Linked Properties + +| Property | Description | Type | Constraints | Supported operations | +| :--------------: | ------------------------------------------------------ | ----------- | -------------------------------- | -------------------- | +| self | This query sort by | QuerySortBy | not null | READ | +| column | The QueryColumn to sort on. | QueryColumn | not null | READ | +| direction | The direction the QueryColumn is to be sorted in. This property is identified by a URI (`urn:openproject-org:api:v3:queries:directions:asc` or `urn:openproject-org:api:v3:queries:directions:desc`) instead of by a URL. | | not null | READ | + +## Local Properties + +| Property | Description | Type | Constraints | Supported operations | +| :--------------: | ------------------------------------------------------ | ----------- | -------------------------------- | -------------------- | +| id | QuerySortBy id | String | not null | READ | +| name | QuerySortBy name | String | not null | READ | + +## Query Sort By [/api/v3/queries/sort_bys/{id}] + ++ Model + + Body + + { + "_type": "QuerySortBy", + "id": "status-asc", + "name": "Status (Ascending)", + "_links": { + "self": { + "href": "/api/v3/queries/sort_bys/status-asc", + "title": "Status (Ascending)" + }, + "column": { + "href": "/api/v3/queries/columns/status", + "title": "Status" + }, + "direction": { + "href": "urn:openproject-org:api:v3:queries:directions:asc", + "title": "Ascending" + } + } + } + +## View Query Sort By [GET] + +Retreive an individual QuerySortBy as identified by the id parameter. + ++ Parameters + + id (required, string, `status-asc`) ... QuerySortBy identifier. The identifier is a combination of the column identifier and the direction. + ++ Response 200 (application/hal+json) + + [Query Sort By][] + ++ Response 403 (application/hal+json) + + Returned if the client does not have sufficient permissions to see it. + + **Required permission:** view work package in any project + + + Body + + { + "_type": "Error", + "errorIdentifier": "urn:openproject-org:api:v3:errors:Unauthenticated", + "message": "You need to be authenticated to access this resource." + } + ++ Response 404 (application/hal+json) + + Returned if the QuerySortBy does not exist. + + + Body + + { + "_type": "Error", + "errorIdentifier": "urn:openproject-org:api:v3:errors:NotFound", + "message": "The specified query does not exist." + } From 65e24558a3b89987db7dab25af5f0f8e676d52ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 2 Feb 2017 11:07:22 +0100 Subject: [PATCH 13/16] Log grape errors when rescuing from them --- lib/api/utilities/grape_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/api/utilities/grape_helper.rb b/lib/api/utilities/grape_helper.rb index c890da1797..5e530218f5 100644 --- a/lib/api/utilities/grape_helper.rb +++ b/lib/api/utilities/grape_helper.rb @@ -54,6 +54,7 @@ module API resp_headers = instance_exec &headers env['api.format'] = 'hal+json' + Rails.logger.error "Grape rescuing from error: #{e}" error_response status: e.code, message: representer.to_json, headers: resp_headers } From ec48fd6f427cece6f401f1eef49f0c56d048f5b5 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 6 Feb 2017 11:01:59 +0100 Subject: [PATCH 14/16] Update supported ruby versions --- doc/operation_guides/system_requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/operation_guides/system_requirements.md b/doc/operation_guides/system_requirements.md index 32c1edb95b..92dee76971 100644 --- a/doc/operation_guides/system_requirements.md +++ b/doc/operation_guides/system_requirements.md @@ -26,7 +26,7 @@ provide any official support for them. ### Dependencies -* __Runtime:__ [Ruby](https://www.ruby-lang.org/en/) Version 2.1.6 +* __Runtime:__ [Ruby](https://www.ruby-lang.org/en/) Version >= 2.2.5, < 2.4 * __Webserver:__ [Apache](http://httpd.apache.org/) or [nginx](http://nginx.org/en/docs/) * __Application server:__ [Phusion Passenger](https://www.phusionpassenger.com/) From 418cd5ec96dee947339e72cd9cbc1b49d274b30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 7 Feb 2017 08:09:59 +0100 Subject: [PATCH 15/16] Loosen engine versions --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 83d863685d..b186072091 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "private": true, "engines": { - "node": "6.9.1", - "npm": "4.0.0" + "node": ">= 6.9.1", + "npm": ">= 4.0.0" } } From beadcc8b389e78fbab9adef2ef0326ead59a703d Mon Sep 17 00:00:00 2001 From: Roman Roelofsen Date: Wed, 8 Feb 2017 14:16:38 +0100 Subject: [PATCH 16/16] Refactoring: angular-rx-utils: - observe() -> observeOnScope() - added observeUntil() --- .../routing/wp-view-base/wp-view-base.controller.ts | 2 +- .../work-packages/work-package-cache.service.test.ts | 4 ++-- .../wp-display-attr/wp-display-attr.directive.ts | 2 +- .../wp-single-view/wp-single-view.directive.ts | 2 +- .../work-packages/wp-subject/wp-subject.directive.ts | 2 +- .../wp-watcher-button/wp-watcher-button.directive.ts | 2 +- frontend/app/components/wp-copy/wp-copy.controller.ts | 2 +- frontend/app/components/wp-create/wp-create.controller.ts | 2 +- .../wp-edit/wp-edit-field/wp-edit-field.directive.ts | 2 +- frontend/app/components/wp-edit/wp-edit-form.directive.ts | 2 +- .../wp-panels/activity-panel/activity-panel.directive.ts | 2 +- .../wp-panels/watchers-panel/watchers-panel.controller.ts | 2 +- .../wp-relations-hierarchy.directive.ts | 4 ++-- .../app/components/wp-relations/wp-relations.directive.ts | 4 ++-- frontend/app/helpers/reactive-fassade.ts | 6 +++++- 15 files changed, 22 insertions(+), 18 deletions(-) diff --git a/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts b/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts index 45ad80d196..fd33b32d6e 100644 --- a/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts +++ b/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts @@ -72,7 +72,7 @@ export class WorkPackageViewController { * Needs to be run explicitly by descendants. */ protected observeWorkPackage() { - this.wpCacheService.loadWorkPackage(this.workPackageId).observe(this.$scope) + this.wpCacheService.loadWorkPackage(this.workPackageId).observeOnScope(this.$scope) .subscribe((wp:WorkPackageResourceInterface) => { this.workPackage = wp; this.init(); diff --git a/frontend/app/components/work-packages/work-package-cache.service.test.ts b/frontend/app/components/work-packages/work-package-cache.service.test.ts index 04bfc18793..5341ac66d9 100644 --- a/frontend/app/components/work-packages/work-package-cache.service.test.ts +++ b/frontend/app/components/work-packages/work-package-cache.service.test.ts @@ -64,7 +64,7 @@ describe('WorkPackageCacheService', () => { wpCacheService.updateWorkPackageList(dummyWorkPackages); let workPackage: WorkPackageResource; - wpCacheService.loadWorkPackage(1).observe(null).subscribe(wp => { + wpCacheService.loadWorkPackage(1).observeOnScope(null).subscribe(wp => { workPackage = wp; expect(workPackage.id).to.eq(1); done(); @@ -100,7 +100,7 @@ describe('WorkPackageCacheService', () => { wpCacheService.updateWorkPackageList([workPackage]); $rootScope.$apply(); - wpCacheService.loadWorkPackage(1).observe(null).subscribe((wp: any) => { + wpCacheService.loadWorkPackage(1).observeOnScope(null).subscribe((wp: any) => { expect(wp.id).to.eq(1); expect(wp.dummy).to.eq(expected); diff --git a/frontend/app/components/work-packages/wp-display-attr/wp-display-attr.directive.ts b/frontend/app/components/work-packages/wp-display-attr/wp-display-attr.directive.ts index 2122884274..7073256ecb 100644 --- a/frontend/app/components/work-packages/wp-display-attr/wp-display-attr.directive.ts +++ b/frontend/app/components/work-packages/wp-display-attr/wp-display-attr.directive.ts @@ -119,7 +119,7 @@ function wpDisplayAttrDirective(wpCacheService:WorkPackageCacheService) { controllers) { if (!scope.$ctrl.customSchema) { - wpCacheService.loadWorkPackage(scope.$ctrl.workPackage.id).observe(scope) + wpCacheService.loadWorkPackage(scope.$ctrl.workPackage.id).observeOnScope(scope) .subscribe((wp: WorkPackageResource) => { scope.$ctrl.updateAttribute(wp); }); diff --git a/frontend/app/components/work-packages/wp-single-view/wp-single-view.directive.ts b/frontend/app/components/work-packages/wp-single-view/wp-single-view.directive.ts index 0ac8159948..a6dae298f3 100644 --- a/frontend/app/components/work-packages/wp-single-view/wp-single-view.directive.ts +++ b/frontend/app/components/work-packages/wp-single-view/wp-single-view.directive.ts @@ -78,7 +78,7 @@ export class WorkPackageSingleViewController { this.init(this.workPackage); } - wpCacheService.loadWorkPackage(wpId).observe($scope).subscribe(wp => this.init(wp)); + wpCacheService.loadWorkPackage(wpId).observeOnScope($scope).subscribe(wp => this.init(wp)); $scope.$on('workPackageUpdatedInEditor', () => { this.wpNotificationsService.showSave(this.workPackage); }); diff --git a/frontend/app/components/work-packages/wp-subject/wp-subject.directive.ts b/frontend/app/components/work-packages/wp-subject/wp-subject.directive.ts index 8c98dc281d..eaf6a7ede9 100644 --- a/frontend/app/components/work-packages/wp-subject/wp-subject.directive.ts +++ b/frontend/app/components/work-packages/wp-subject/wp-subject.directive.ts @@ -37,7 +37,7 @@ export class WorkPackageSubjectController { protected $stateParams, protected wpCacheService) { if (!this.workPackage) { - wpCacheService.loadWorkPackage($stateParams.workPackageId).observe($scope) + wpCacheService.loadWorkPackage($stateParams.workPackageId).observeOnScope($scope) .subscribe((wp: WorkPackageResource) => { this.workPackage = wp; }); diff --git a/frontend/app/components/work-packages/wp-watcher-button/wp-watcher-button.directive.ts b/frontend/app/components/work-packages/wp-watcher-button/wp-watcher-button.directive.ts index 20f00b6f9c..898e31223f 100644 --- a/frontend/app/components/work-packages/wp-watcher-button/wp-watcher-button.directive.ts +++ b/frontend/app/components/work-packages/wp-watcher-button/wp-watcher-button.directive.ts @@ -45,7 +45,7 @@ export class WorkPackageWatcherButtonController { public I18n, public wpCacheService:WorkPackageCacheService) { - wpCacheService.loadWorkPackage( this.workPackage.id).observe($scope) + wpCacheService.loadWorkPackage( this.workPackage.id).observeOnScope($scope) .subscribe((wp: WorkPackageResourceInterface) => { this.workPackage = wp; this.setWatchStatus(); diff --git a/frontend/app/components/wp-copy/wp-copy.controller.ts b/frontend/app/components/wp-copy/wp-copy.controller.ts index 7be5b996dc..58dfc7a0cc 100644 --- a/frontend/app/components/wp-copy/wp-copy.controller.ts +++ b/frontend/app/components/wp-copy/wp-copy.controller.ts @@ -37,7 +37,7 @@ export class WorkPackageCopyController extends WorkPackageCreateController { protected newWorkPackageFromParams(stateParams) { var deferred = this.$q.defer(); - this.wpCacheService.loadWorkPackage(stateParams.copiedFromWorkPackageId).observe(this.$scope) + this.wpCacheService.loadWorkPackage(stateParams.copiedFromWorkPackageId).observeOnScope(this.$scope) .subscribe((wp:WorkPackageResourceInterface) => { this.createCopyFrom(wp).then(newWorkPackage => { deferred.resolve(newWorkPackage); diff --git a/frontend/app/components/wp-create/wp-create.controller.ts b/frontend/app/components/wp-create/wp-create.controller.ts index cf777168bd..e60901d287 100644 --- a/frontend/app/components/wp-create/wp-create.controller.ts +++ b/frontend/app/components/wp-create/wp-create.controller.ts @@ -82,7 +82,7 @@ export class WorkPackageCreateController { wpCacheService.updateWorkPackage(wp); if ($state.params.parent_id) { - wpCacheService.loadWorkPackage($state.params.parent_id).observe($scope) + wpCacheService.loadWorkPackage($state.params.parent_id).observeOnScope($scope) .subscribe(parent => { this.parentWorkPackage = parent; this.newWorkPackage.parent = parent; diff --git a/frontend/app/components/wp-edit/wp-edit-field/wp-edit-field.directive.ts b/frontend/app/components/wp-edit/wp-edit-field/wp-edit-field.directive.ts index 779eac0875..0df240a11c 100644 --- a/frontend/app/components/wp-edit/wp-edit-field/wp-edit-field.directive.ts +++ b/frontend/app/components/wp-edit/wp-edit-field/wp-edit-field.directive.ts @@ -350,7 +350,7 @@ function wpEditField(wpCacheService: WorkPackageCacheService) { controllers[1].formCtrl = formCtrl; formCtrl.registerField(scope.vm); - wpCacheService.loadWorkPackage(formCtrl.workPackage.id).observe(scope) + wpCacheService.loadWorkPackage(formCtrl.workPackage.id).observeOnScope(scope) .subscribe((wp: WorkPackageResource) => { scope.vm.workPackage = wp; scope.vm.initializeField(); diff --git a/frontend/app/components/wp-edit/wp-edit-form.directive.ts b/frontend/app/components/wp-edit/wp-edit-form.directive.ts index 18a23e80bf..96a14905d3 100644 --- a/frontend/app/components/wp-edit/wp-edit-form.directive.ts +++ b/frontend/app/components/wp-edit/wp-edit-form.directive.ts @@ -57,7 +57,7 @@ export class WorkPackageEditFormController { wpEditModeState.register(this); } - states.workPackages.get(this.workPackage.id.toString()).observe($scope) + states.workPackages.get(this.workPackage.id.toString()).observeOnScope($scope) .subscribe((wp: WorkPackageResource) => { this.workPackage = wp; }); diff --git a/frontend/app/components/wp-panels/activity-panel/activity-panel.directive.ts b/frontend/app/components/wp-panels/activity-panel/activity-panel.directive.ts index 00abea0b3c..02e9f3a3e4 100644 --- a/frontend/app/components/wp-panels/activity-panel/activity-panel.directive.ts +++ b/frontend/app/components/wp-panels/activity-panel/activity-panel.directive.ts @@ -42,7 +42,7 @@ export class ActivityPanelController { this.reverse = wpActivity.order === 'asc'; - wpCacheService.loadWorkPackage( this.workPackage.id).observe($scope) + wpCacheService.loadWorkPackage( this.workPackage.id).observeOnScope($scope) .subscribe((wp:WorkPackageResourceInterface) => { this.workPackage = wp; this.wpActivity.aggregateActivities(this.workPackage).then(activities => { diff --git a/frontend/app/components/wp-panels/watchers-panel/watchers-panel.controller.ts b/frontend/app/components/wp-panels/watchers-panel/watchers-panel.controller.ts index 7b172a9d13..4df8366fdd 100644 --- a/frontend/app/components/wp-panels/watchers-panel/watchers-panel.controller.ts +++ b/frontend/app/components/wp-panels/watchers-panel/watchers-panel.controller.ts @@ -65,7 +65,7 @@ export class WatchersPanelController { return; } - wpCacheService.loadWorkPackage( this.workPackage.id).observe($scope) + wpCacheService.loadWorkPackage( this.workPackage.id).observeOnScope($scope) .subscribe((wp: WorkPackageResourceInterface) => { this.workPackage = wp; this.loadCurrentWatchers(); diff --git a/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive.ts b/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive.ts index 12e354d0cc..4541c13ef7 100644 --- a/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive.ts +++ b/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive.ts @@ -47,7 +47,7 @@ export class WorkPackageRelationsHierarchyController { this.wpCacheService .loadWorkPackage( this.workPackage.id) - .observe(this.$scope) + .observeOnScope(this.$scope) .subscribe((wp:WorkPackageResourceInterface) => { this.workPackage = wp; this.loadParent(); @@ -72,7 +72,7 @@ export class WorkPackageRelationsHierarchyController { this.wpCacheService .loadWorkPackage(this.workPackage.parentId) - .observe(this.$scope) + .observeOnScope(this.$scope) .take(1) .subscribe((parent:WorkPackageResourceInterface) => { this.workPackage.parent = parent; diff --git a/frontend/app/components/wp-relations/wp-relations.directive.ts b/frontend/app/components/wp-relations/wp-relations.directive.ts index a230acb8f7..0f2ce04157 100644 --- a/frontend/app/components/wp-relations/wp-relations.directive.ts +++ b/frontend/app/components/wp-relations/wp-relations.directive.ts @@ -59,7 +59,7 @@ export class WorkPackageRelationsController { // Listen for changes to this WP. this.wpCacheService .loadWorkPackage( this.workPackage.id) - .observe(this.$scope) + .observeOnScope(this.$scope) .subscribe((wp:WorkPackageResourceInterface) => { this.workPackage = wp; this.workPackage.relations.$load().then(this.loadRelations.bind(this)); @@ -67,7 +67,7 @@ export class WorkPackageRelationsController { } protected getRelatedWorkPackages(workPackageIds:number[]) { - let observablesToGetZipped = workPackageIds.map(wpId => this.wpCacheService.loadWorkPackage(wpId).observe(this.$scope)); + let observablesToGetZipped = workPackageIds.map(wpId => this.wpCacheService.loadWorkPackage(wpId).observeOnScope(this.$scope)); if (observablesToGetZipped.length > 1) { return Observable diff --git a/frontend/app/helpers/reactive-fassade.ts b/frontend/app/helpers/reactive-fassade.ts index 8c030438d7..6d7caf28ad 100644 --- a/frontend/app/helpers/reactive-fassade.ts +++ b/frontend/app/helpers/reactive-fassade.ts @@ -108,10 +108,14 @@ export class State extends StoreElement { return this.observable.take(1).toPromise(); } - public observe(scope: IScope): Observable { + public observeOnScope(scope: IScope): Observable { return this.scopedObservable(scope); } + public observeUntil(unsubscribeNotifier: Observable): Observable { + return this.observable.takeUntil(unsubscribeNotifier); + } + public observeCleared(scope: IScope): Observable { return scope ? scopedObservable(scope, this.cleared.asObservable()) : this.cleared.asObservable(); }