From b34310ee482d0761f639296353bdcff1193c8128 Mon Sep 17 00:00:00 2001 From: ulferts Date: Thu, 2 Dec 2021 15:03:15 +0100 Subject: [PATCH] add POST /api/v3/views/team_planner --- app/contracts/views/base_contract.rb | 24 +++- config/constants/views.rb | 68 +++++++++++ config/initializers/register_views.rb | 31 +++++ lib/api/v3/utilities/path_helper.rb | 4 +- lib/api/v3/views/view_representer.rb | 2 +- lib/api/v3/views/views_api.rb | 12 +- lib/open_project/plugins/acts_as_op_engine.rb | 5 + .../team_planner/views/contract_strategy.rb | 24 ++++ .../lib/open_project/team_planner/engine.rb | 5 +- .../create_contract_team_planner_spec.rb | 68 +++++++++++ .../lib/api/v3/utilities/path_helper_spec.rb | 0 .../api/v3/views/create_resource_spec.rb | 111 ++++++++++++++++++ ...eate_contract_work_packages_table_spec.rb} | 0 .../views/shared_contract_examples.rb | 28 +++-- spec/contracts/views/update_contract_spec.rb | 5 +- spec/lib/api/v3/utilities/path_helper_spec.rb | 4 +- .../api/v3/views/create_resource_spec.rb | 10 +- .../api/v3/views/index_resource_spec.rb | 1 - 18 files changed, 370 insertions(+), 32 deletions(-) create mode 100644 config/constants/views.rb create mode 100644 config/initializers/register_views.rb create mode 100644 modules/team_planner/app/contracts/team_planner/views/contract_strategy.rb create mode 100644 modules/team_planner/spec/contracts/views/create_contract_team_planner_spec.rb delete mode 100644 modules/team_planner/spec/lib/api/v3/utilities/path_helper_spec.rb create mode 100644 modules/team_planner/spec/requests/api/v3/views/create_resource_spec.rb rename spec/contracts/views/{create_contract_spec.rb => create_contract_work_packages_table_spec.rb} (100%) diff --git a/app/contracts/views/base_contract.rb b/app/contracts/views/base_contract.rb index 93442749a3..49d6e16118 100644 --- a/app/contracts/views/base_contract.rb +++ b/app/contracts/views/base_contract.rb @@ -34,26 +34,40 @@ module Views validate :query_present, :query_manageable + def valid?(*_args) + super + + # Registered views can have an additional contract configured that + # needs to validate additionally. E.g. a contract can have additional permission checks. + if (strategy_class = Constants::Views.contract_strategy(model.type)) + strategy = strategy_class.new(model, user) + strategy.valid? + + errors.merge!(strategy.errors) + end + + errors.empty? + end + private def type_allowed - # TODO: fetch from registry - unless ['work_packages_table'].include?(model.type) + unless Constants::Views.registered?(model.type) errors.add(:type, :inclusion) end end def query_present - unless model.query + if model.query.blank? errors.add(:query, :blank) end end def query_manageable - return unless model.query + return if model.query.blank? if query_visible? - errors.add(:query, :error_unauthorized) unless query_permissions? + errors.add(:base, :error_unauthorized) unless query_permissions? else errors.add(:query, :does_not_exist) end diff --git a/config/constants/views.rb b/config/constants/views.rb new file mode 100644 index 0000000000..bb43982819 --- /dev/null +++ b/config/constants/views.rb @@ -0,0 +1,68 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Constants + module Views + class << self + def add(type, + contract_strategy: nil) + @registered ||= {} + + @registered[canonical_type(type)] = { contract_strategy: contract_strategy } + end + + def registered_types + registered.keys + end + + def registered?(type) + type && registered_types.include?(canonical_type(type)) + end + + def type(type) + searched_type = canonical_type(type) + + registered_types.find { |type| type == searched_type } + end + + def contract_strategy(type) + if registered?(type) + registered[canonical_type(type)][:contract_strategy]&.constantize + end + end + + attr_reader :registered + + private + + def canonical_type(type) + type.to_s.camelize.to_sym + end + end + end +end diff --git a/config/initializers/register_views.rb b/config/initializers/register_views.rb new file mode 100644 index 0000000000..78ec3178fc --- /dev/null +++ b/config/initializers/register_views.rb @@ -0,0 +1,31 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require Rails.root.join('config/constants/views') + +Constants::Views.add :WorkPackagesTable diff --git a/lib/api/v3/utilities/path_helper.rb b/lib/api/v3/utilities/path_helper.rb index 0bdb0af215..d5c67c098c 100644 --- a/lib/api/v3/utilities/path_helper.rb +++ b/lib/api/v3/utilities/path_helper.rb @@ -420,8 +420,8 @@ module API index :view show :view - def self.views_work_packages_table - "#{views}/work_packages_table" + def self.views_type(type) + "#{views}/#{type}" end def self.versions_available_projects diff --git a/lib/api/v3/views/view_representer.rb b/lib/api/v3/views/view_representer.rb index 882ce9061d..fb164f911b 100644 --- a/lib/api/v3/views/view_representer.rb +++ b/lib/api/v3/views/view_representer.rb @@ -88,7 +88,7 @@ module API date_time_property :updated_at def _type - 'Views::WorkPackagesTable' + "Views::#{Constants::Views.type(represented.type)}" end end end diff --git a/lib/api/v3/views/views_api.rb b/lib/api/v3/views/views_api.rb index 3c7a1ef2e8..4037a83c8c 100644 --- a/lib/api/v3/views/views_api.rb +++ b/lib/api/v3/views/views_api.rb @@ -35,11 +35,17 @@ module API .new(model: View) .mount - resources :work_packages_table do + route_param :type_name, type: String, desc: 'View name' do + after_validation do + @type = params['type_name'] + + raise API::Errors::NotFound unless Constants::Views.registered?(@type) + end + post &::API::V3::Utilities::Endpoints::Create .new(model: View, - params_modifier: ->(params) { - params.merge(type: 'work_packages_table') + params_modifier: ->(body_params) { + body_params.merge(type: @type) }) .mount end diff --git a/lib/open_project/plugins/acts_as_op_engine.rb b/lib/open_project/plugins/acts_as_op_engine.rb index 7f5020e463..1965ffa339 100644 --- a/lib/open_project/plugins/acts_as_op_engine.rb +++ b/lib/open_project/plugins/acts_as_op_engine.rb @@ -29,6 +29,7 @@ require_dependency 'open_project/ui/extensible_tabs' require_dependency 'config/constants/api_patch_registry' require_dependency 'config/constants/open_project/activity' +require_dependency 'config/constants/views' module OpenProject::Plugins module ActsAsOpEngine @@ -215,6 +216,10 @@ module OpenProject::Plugins ::OpenProject::Ui::ExtensibleTabs.add(key, name: name, partial: partial, path: path, label: label, only_if: only_if) end + def add_view(type, contract_strategy: nil) + Constants::Views.add(type, contract_strategy: contract_strategy) + end + def add_api_path(path_name, &block) config.to_prepare do ::API::V3::Utilities::PathHelper::ApiV3Path.class_eval do diff --git a/modules/team_planner/app/contracts/team_planner/views/contract_strategy.rb b/modules/team_planner/app/contracts/team_planner/views/contract_strategy.rb new file mode 100644 index 0000000000..951b26d454 --- /dev/null +++ b/modules/team_planner/app/contracts/team_planner/views/contract_strategy.rb @@ -0,0 +1,24 @@ +module ::TeamPlanner + module Views + class ContractStrategy < ::BaseContract + validate :manageable + + private + + def manageable + return if model.query.blank? + + errors.add(:base, :error_unauthorized) unless query_permissions? + end + + def query_permissions? + # TODO: This currently does not differentiate between public and private queries since it isn't specified yet. + user_allowed_on_query?(:manage_team_planner) + end + + def user_allowed_on_query?(permission) + user.allowed_to?(permission, model.query.project, global: model.query.project.nil?) + end + end + end +end diff --git a/modules/team_planner/lib/open_project/team_planner/engine.rb b/modules/team_planner/lib/open_project/team_planner/engine.rb index 88521dfd9b..2c088cce8d 100644 --- a/modules/team_planner/lib/open_project/team_planner/engine.rb +++ b/modules/team_planner/lib/open_project/team_planner/engine.rb @@ -33,7 +33,7 @@ module OpenProject::TeamPlanner dependencies: %i[view_work_packages] permission :manage_team_planner, { 'team_planner/team_planner': %i[index] }, - dependencies: %i[view_team_planner add_work_packages edit_work_packages manage_public_queries] + dependencies: %i[view_team_planner add_work_packages edit_work_packages save_queries manage_public_queries] end menu :project_menu, @@ -44,5 +44,8 @@ module OpenProject::TeamPlanner icon: 'icon2 icon-calendar', badge: 'label_menu_badge.pre_alpha' end + + add_view :TeamPlanner, + contract_strategy: 'TeamPlanner::Views::ContractStrategy' end end diff --git a/modules/team_planner/spec/contracts/views/create_contract_team_planner_spec.rb b/modules/team_planner/spec/contracts/views/create_contract_team_planner_spec.rb new file mode 100644 index 0000000000..5199ded703 --- /dev/null +++ b/modules/team_planner/spec/contracts/views/create_contract_team_planner_spec.rb @@ -0,0 +1,68 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' +require 'contracts/shared/model_contract_shared_context' +require 'contracts/views/shared_contract_examples' + +describe Views::CreateContract do + it_behaves_like 'view contract', true do + let(:view) do + View.new(query: view_query, + type: view_type) + end + let(:view_type) do + 'team_planner' + end + let(:permissions) { %i[view_work_packages save_queries manage_team_planner] } + + subject(:contract) do + described_class.new(view, current_user) + end + + describe 'validation' do + context 'with the type being nil' do + let(:view_type) { nil } + + it_behaves_like 'contract is invalid', type: :inclusion + end + + context 'with the type not being one of the configured' do + let(:view_type) { 'blubs' } + + it_behaves_like 'contract is invalid', type: :inclusion + end + + context 'without the :manage_team_planner permission' do + let(:permissions) { %i[view_work_packages save_queries] } + + it_behaves_like 'contract is invalid', base: :error_unauthorized + end + end + end +end diff --git a/modules/team_planner/spec/lib/api/v3/utilities/path_helper_spec.rb b/modules/team_planner/spec/lib/api/v3/utilities/path_helper_spec.rb deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/modules/team_planner/spec/requests/api/v3/views/create_resource_spec.rb b/modules/team_planner/spec/requests/api/v3/views/create_resource_spec.rb new file mode 100644 index 0000000000..ba2fe9fab3 --- /dev/null +++ b/modules/team_planner/spec/requests/api/v3/views/create_resource_spec.rb @@ -0,0 +1,111 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2020 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. + +require 'spec_helper' + +describe ::API::V3::Views::ViewsAPI, + 'create', + content_type: :json, + type: :request do + include API::V3::Utilities::PathHelper + + shared_let(:permitted_user) { FactoryBot.create(:user) } + shared_let(:role) do + FactoryBot.create(:role, + permissions: %w[view_work_packages + save_queries + manage_public_queries + manage_team_planner]) + end + shared_let(:project) do + FactoryBot.create(:project, + members: { permitted_user => role }) + end + shared_let(:public_query) do + FactoryBot.create(:query, + project: project, + is_public: true) + end + + let(:additional_setup) do + # to be overwritten by some specs + end + + let(:body) do + { + _links: { + query: { + href: api_v3_paths.query(public_query.id) + } + } + }.to_json + end + + let(:send_request) do + post api_v3_paths.views_type('team_planner'), body + end + + current_user { permitted_user } + + subject(:response) { last_response } + + before do + additional_setup + + send_request + end + + describe 'POST /api/v3/views/team_planner' do + context 'with a user allowed to save the query' do + it 'returns 201 CREATED' do + expect(response.status) + .to eq(201) + end + + it 'returns the view' do + expect(response.body) + .to be_json_eql('Views::TeamPlanner'.to_json) + .at_path('_type') + + expect(response.body) + .to be_json_eql(View.last.id.to_json) + .at_path('id') + end + end + + context 'with a user not allowed to manage team planners' do + let(:additional_setup) do + role.update_attribute(:permissions, + %w[view_work_packages + save_queries + manage_public_queries]) + end + + it_behaves_like 'unauthorized access' + end + end +end diff --git a/spec/contracts/views/create_contract_spec.rb b/spec/contracts/views/create_contract_work_packages_table_spec.rb similarity index 100% rename from spec/contracts/views/create_contract_spec.rb rename to spec/contracts/views/create_contract_work_packages_table_spec.rb diff --git a/spec/contracts/views/shared_contract_examples.rb b/spec/contracts/views/shared_contract_examples.rb index 2e61081551..43b8b9f170 100644 --- a/spec/contracts/views/shared_contract_examples.rb +++ b/spec/contracts/views/shared_contract_examples.rb @@ -29,7 +29,7 @@ require 'spec_helper' require 'contracts/shared/model_contract_shared_context' -shared_examples_for 'view contract' do +shared_examples_for 'view contract' do |disabled_permission_checks| include_context 'ModelContract shared context' let(:current_user) do @@ -87,23 +87,25 @@ shared_examples_for 'view contract' do it_behaves_like 'contract is valid' end - context 'with the query being private, the user being the query user and not having the :save_queries permission' do - let(:permissions) { %i[view_work_packages] } + unless disabled_permission_checks + context 'with the query being private, the user being the query user and not having the :save_queries permission' do + let(:permissions) { %i[view_work_packages] } - it_behaves_like 'contract is invalid', query: :error_unauthorized - end + it_behaves_like 'contract is invalid', base: :error_unauthorized + end - context 'with the query being public, the user being the query user but only having the :save_queries permission' do - let(:query_public) { true } + context 'with the query being public, the user being the query user but only having the :save_queries permission' do + let(:query_public) { true } - it_behaves_like 'contract is invalid', query: :error_unauthorized - end + it_behaves_like 'contract is invalid', base: :error_unauthorized + end - context 'with the query being public, the user not being the query user but having the :manage_public_queries permission' do - let(:query_public) { true } - let(:permissions) { %i[view_work_packages manage_public_queries] } + context 'with the query being public, the user not being the query user but having the :manage_public_queries permission' do + let(:query_public) { true } + let(:permissions) { %i[view_work_packages manage_public_queries] } - it_behaves_like 'contract is valid' + it_behaves_like 'contract is valid' + end end end end diff --git a/spec/contracts/views/update_contract_spec.rb b/spec/contracts/views/update_contract_spec.rb index 39dacb6289..2ad657e7a4 100644 --- a/spec/contracts/views/update_contract_spec.rb +++ b/spec/contracts/views/update_contract_spec.rb @@ -46,10 +46,9 @@ describe Views::UpdateContract do describe 'validation' do context 'with the type being changed' do - # TODO: once a second type is defined, use that here - let(:view_type) { 'another_type' } + let(:view_type) { 'team_planner' } - it_behaves_like 'contract is invalid', type: [:error_readonly, :inclusion] + it_behaves_like 'contract is invalid', type: %i[error_readonly] 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 af5c783ec4..07bb9b8117 100644 --- a/spec/lib/api/v3/utilities/path_helper_spec.rb +++ b/spec/lib/api/v3/utilities/path_helper_spec.rb @@ -503,8 +503,8 @@ describe ::API::V3::Utilities::PathHelper do it_behaves_like 'show', :view it_behaves_like 'index', :view - describe '#views_work_packages_table' do - subject { helper.views_work_packages_table } + describe '#views_type' do + subject { helper.views_type('work_packages_table') } it_behaves_like 'api v3 path', '/views/work_packages_table' end diff --git a/spec/requests/api/v3/views/create_resource_spec.rb b/spec/requests/api/v3/views/create_resource_spec.rb index 4303a27e71..36c2a14e56 100644 --- a/spec/requests/api/v3/views/create_resource_spec.rb +++ b/spec/requests/api/v3/views/create_resource_spec.rb @@ -61,7 +61,7 @@ describe ::API::V3::Views::ViewsAPI, end let(:send_request) do - post api_v3_paths.views_work_packages_table, body + post api_v3_paths.views_type('work_packages_table'), body end current_user { permitted_user } @@ -106,4 +106,12 @@ describe ::API::V3::Views::ViewsAPI, end end end + + describe 'POST /api/v3/views/bogus' do + let(:send_request) do + post api_v3_paths.views_type('bogus'), body + end + + it_behaves_like 'not found' + end end diff --git a/spec/requests/api/v3/views/index_resource_spec.rb b/spec/requests/api/v3/views/index_resource_spec.rb index 7f0fde09b9..6d89d87e92 100644 --- a/spec/requests/api/v3/views/index_resource_spec.rb +++ b/spec/requests/api/v3/views/index_resource_spec.rb @@ -32,7 +32,6 @@ describe ::API::V3::Views::ViewsAPI, 'index', type: :request, content_type: :json do - include API::V3::Utilities::PathHelper shared_let(:permitted_user) { FactoryBot.create(:user) }