From a3459b8bda32df72f2316a0e8cdfc4b360a3ca68 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Fri, 29 May 2026 10:47:07 +0200 Subject: [PATCH 01/12] [#73887] Add backlog buckets to API --- .../display/display-field.initializer.ts | 1 + .../fields/edit/edit-field.initializer.ts | 1 + .../app/models/queries/backlog_buckets.rb | 35 ++++++ .../backlog_buckets/backlog_bucket_query.rb | 46 ++++++++ .../backlog_buckets/orders/default_order.rb | 37 +++++++ .../backlog_bucket_collection_representer.rb | 38 +++++++ .../backlog_bucket_representer.rb | 61 +++++++++++ .../v3/backlog_buckets/backlog_buckets_api.rb | 51 +++++++++ .../backlog_buckets_by_project_api.rb | 50 +++++++++ .../lib/open_project/backlogs/engine.rb | 21 ++++ .../patches/api/work_package_representer.rb | 27 +++++ .../api/work_package_schema_representer.rb | 14 +++ ...cklog_bucket_representer_rendering_spec.rb | 103 ++++++++++++++++++ .../work_package_schema_representer_spec.rb | 24 ++++ ...work_package_representer_rendering_spec.rb | 52 ++++++++- .../v3/backlog_buckets/index_resource_spec.rb | 90 +++++++++++++++ .../project_index_resource_spec.rb | 72 ++++++++++++ .../v3/backlog_buckets/show_resource_spec.rb | 74 +++++++++++++ 18 files changed, 796 insertions(+), 1 deletion(-) create mode 100644 modules/backlogs/app/models/queries/backlog_buckets.rb create mode 100644 modules/backlogs/app/models/queries/backlog_buckets/backlog_bucket_query.rb create mode 100644 modules/backlogs/app/models/queries/backlog_buckets/orders/default_order.rb create mode 100644 modules/backlogs/lib/api/v3/backlog_buckets/backlog_bucket_collection_representer.rb create mode 100644 modules/backlogs/lib/api/v3/backlog_buckets/backlog_bucket_representer.rb create mode 100644 modules/backlogs/lib/api/v3/backlog_buckets/backlog_buckets_api.rb create mode 100644 modules/backlogs/lib/api/v3/backlog_buckets/backlog_buckets_by_project_api.rb create mode 100644 modules/backlogs/spec/lib/api/v3/backlog_buckets/backlog_bucket_representer_rendering_spec.rb create mode 100644 modules/backlogs/spec/requests/api/v3/backlog_buckets/index_resource_spec.rb create mode 100644 modules/backlogs/spec/requests/api/v3/backlog_buckets/project_index_resource_spec.rb create mode 100644 modules/backlogs/spec/requests/api/v3/backlog_buckets/show_resource_spec.rb diff --git a/frontend/src/app/shared/components/fields/display/display-field.initializer.ts b/frontend/src/app/shared/components/fields/display/display-field.initializer.ts index 7b38b7ad9285..a1a84fd82f9e 100644 --- a/frontend/src/app/shared/components/fields/display/display-field.initializer.ts +++ b/frontend/src/app/shared/components/fields/display/display-field.initializer.ts @@ -107,6 +107,7 @@ export function initializeCoreDisplayFields(displayFieldService:DisplayFieldServ 'TimeEntriesActivity', 'Version', 'Category', + 'BacklogBucket', 'Sprint', 'CustomField::Hierarchy::Item', 'CustomOption', diff --git a/frontend/src/app/shared/components/fields/edit/edit-field.initializer.ts b/frontend/src/app/shared/components/fields/edit/edit-field.initializer.ts index 118b58bdb885..a8c953547e3a 100644 --- a/frontend/src/app/shared/components/fields/edit/edit-field.initializer.ts +++ b/frontend/src/app/shared/components/fields/edit/edit-field.initializer.ts @@ -100,6 +100,7 @@ export function initializeCoreEditFields(editFieldService:EditFieldService, sele 'Version', 'TimeEntriesActivity', 'Category', + 'BacklogBucket', 'Sprint', 'CustomOption', 'CustomField::Hierarchy::Item', diff --git a/modules/backlogs/app/models/queries/backlog_buckets.rb b/modules/backlogs/app/models/queries/backlog_buckets.rb new file mode 100644 index 000000000000..62cf00316f20 --- /dev/null +++ b/modules/backlogs/app/models/queries/backlog_buckets.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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 Queries::BacklogBuckets + ::Queries::Register.register(BacklogBucketQuery) do + order Orders::DefaultOrder + end +end diff --git a/modules/backlogs/app/models/queries/backlog_buckets/backlog_bucket_query.rb b/modules/backlogs/app/models/queries/backlog_buckets/backlog_bucket_query.rb new file mode 100644 index 000000000000..33e2f2289db3 --- /dev/null +++ b/modules/backlogs/app/models/queries/backlog_buckets/backlog_bucket_query.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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 Queries + module BacklogBuckets + class BacklogBucketQuery + include ::Queries::BaseQuery + include ::Queries::UnpersistedQuery + + def self.model + ::BacklogBucket + end + + def default_scope + ::BacklogBucket.visible(User.current) + end + end + end +end diff --git a/modules/backlogs/app/models/queries/backlog_buckets/orders/default_order.rb b/modules/backlogs/app/models/queries/backlog_buckets/orders/default_order.rb new file mode 100644 index 000000000000..30311b27dd43 --- /dev/null +++ b/modules/backlogs/app/models/queries/backlog_buckets/orders/default_order.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +# ++ + +class Queries::BacklogBuckets::Orders::DefaultOrder < Queries::Orders::Base + self.model = BacklogBucket + + def self.key + /\A(id|name)\z/ + end +end diff --git a/modules/backlogs/lib/api/v3/backlog_buckets/backlog_bucket_collection_representer.rb b/modules/backlogs/lib/api/v3/backlog_buckets/backlog_bucket_collection_representer.rb new file mode 100644 index 000000000000..23724dad31ab --- /dev/null +++ b/modules/backlogs/lib/api/v3/backlog_buckets/backlog_bucket_collection_representer.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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 API + module V3 + module BacklogBuckets + class BacklogBucketCollectionRepresenter < ::API::Decorators::OffsetPaginatedCollection + end + end + end +end diff --git a/modules/backlogs/lib/api/v3/backlog_buckets/backlog_bucket_representer.rb b/modules/backlogs/lib/api/v3/backlog_buckets/backlog_bucket_representer.rb new file mode 100644 index 000000000000..ca29a4d75e02 --- /dev/null +++ b/modules/backlogs/lib/api/v3/backlog_buckets/backlog_bucket_representer.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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 "roar/decorator" +require "roar/json/hal" + +module API + module V3 + module BacklogBuckets + class BacklogBucketRepresenter < ::API::Decorators::Single + include API::Decorators::LinkedResource + include API::V3::Workspaces::LinkedResource + include API::Decorators::DateProperty + + self_link + + associated_project as: :definingWorkspace + + property :id, + render_nil: true + + property :name, + render_nil: true + + date_time_property :created_at + date_time_property :updated_at + + def _type + "BacklogBucket" + end + end + end + end +end diff --git a/modules/backlogs/lib/api/v3/backlog_buckets/backlog_buckets_api.rb b/modules/backlogs/lib/api/v3/backlog_buckets/backlog_buckets_api.rb new file mode 100644 index 000000000000..c7621343206f --- /dev/null +++ b/modules/backlogs/lib/api/v3/backlog_buckets/backlog_buckets_api.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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 API + module V3 + module BacklogBuckets + class BacklogBucketsAPI < ::API::OpenProjectAPI + resources :backlog_buckets do + get &::API::V3::Utilities::Endpoints::Index + .new(model: BacklogBucket) + .mount + + route_param :id, type: Integer, desc: "Backlog Bucket ID" do + after_validation do + @backlog_bucket = BacklogBucket.visible(current_user).find(params[:id]) + end + + get &::API::V3::Utilities::Endpoints::Show.new(model: BacklogBucket).mount + end + end + end + end + end +end diff --git a/modules/backlogs/lib/api/v3/backlog_buckets/backlog_buckets_by_project_api.rb b/modules/backlogs/lib/api/v3/backlog_buckets/backlog_buckets_by_project_api.rb new file mode 100644 index 000000000000..a926b9aaccbb --- /dev/null +++ b/modules/backlogs/lib/api/v3/backlog_buckets/backlog_buckets_by_project_api.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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 API + module V3 + module BacklogBuckets + class BacklogBucketsByProjectAPI < ::API::OpenProjectAPI + resources :backlog_buckets do + after_validation do + authorize_in_project(:view_sprints, project: @project) + end + + get &::API::V3::Utilities::Endpoints::Index + .new( + model: BacklogBucket, + scope: -> { BacklogBucket.where(project: @project).visible.order_alphabetically } + ) + .mount + end + end + end + end +end diff --git a/modules/backlogs/lib/open_project/backlogs/engine.rb b/modules/backlogs/lib/open_project/backlogs/engine.rb index 85f0baa0baae..442274a23393 100644 --- a/modules/backlogs/lib/open_project/backlogs/engine.rb +++ b/modules/backlogs/lib/open_project/backlogs/engine.rb @@ -173,12 +173,26 @@ def self.settings "#{root}/projects/#{id}/sprints" end + add_api_path :backlog_buckets do + "#{root}/backlog_buckets" + end + + add_api_path :backlog_bucket do |id| + "#{root}/backlog_buckets/#{id}" + end + + add_api_path :project_backlog_buckets do |id| + "#{root}/projects/#{id}/backlog_buckets" + end + add_api_endpoint "API::V3::Root" do mount ::API::V3::Sprints::SprintsAPI + mount ::API::V3::BacklogBuckets::BacklogBucketsAPI end add_api_endpoint "API::V3::Projects::ProjectsAPI", :id do mount ::API::V3::Sprints::SprintsByProjectAPI + mount ::API::V3::BacklogBuckets::BacklogBucketsByProjectAPI end initializer "openproject_backlogs.event_subscriptions" do @@ -224,9 +238,16 @@ def self.settings ::Type.add_constraint attribute, ->(_type, project: nil) { project.nil? || project.backlogs_enabled? } end + story_and_sprint_permission = ->(_type, project: nil) do + project.nil? || User.current.allowed_in_project?(:view_sprints, project) + end + + ::Type.add_constraint :backlog_bucket, story_and_sprint_permission + ::Type.add_default_mapping(:estimates_and_progress, :story_points) ::Type.add_default_mapping(:other, :position) ::Type.add_default_mapping(:details, :sprint) + ::Type.add_default_mapping(:details, :backlog_bucket) ::Queries::Register.register(::Query) do filter Queries::WorkPackages::Filter::SprintFilter diff --git a/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_representer.rb b/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_representer.rb index 6b9783ce1f35..40439ffd5d8f 100644 --- a/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_representer.rb +++ b/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_representer.rb @@ -72,6 +72,33 @@ def extension # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity end end, setter: associated_resource_default_setter(:sprint, :sprint, :sprint) + + resource :backlog_bucket, + link_cache_if: ->(*) { + represented.project.present? && + current_user.allowed_in_project?(:view_sprints, represented.project) + }, + link: ->(*) { + if represented.backlog_bucket.present? + { + href: api_v3_paths.backlog_bucket(represented.backlog_bucket_id), + title: represented.backlog_bucket.name + } + else + { + href: nil + } + end + }, + getter: ->(*) do + if embed_links && + represented.project.present? && + represented.backlog_bucket.present? && + current_user.allowed_in_project?(:view_sprints, represented.project) + ::API::V3::BacklogBuckets::BacklogBucketRepresenter.create(represented.backlog_bucket, current_user:) + end + end, + setter: associated_resource_default_setter(:backlog_bucket, :backlog_bucket, :backlog_bucket) end end end diff --git a/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_schema_representer.rb b/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_schema_representer.rb index 813790bd4d78..bb9f518da85c 100644 --- a/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_schema_representer.rb +++ b/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_schema_representer.rb @@ -67,6 +67,20 @@ def extension "#{api_v3_paths.project_sprints(represented.project_id)}?filters=#{filters}&pageSize=-1" } + schema_with_allowed_link :backlog_bucket, + has_default: false, + required: false, + show_if: ->(*) { + current_user.allowed_in_project?(:view_sprints, represented.project) && + backlogs_constraint_passed?(:backlog_bucket) + }, + href_callback: ->(*) { + filters = CGI.escape(JSON.dump([])) + query = "filters=#{filters}&pageSize=-1" + + "#{api_v3_paths.project_backlog_buckets(represented.project_id)}?#{query}" + } + define_method :backlogs_constraint_passed? do |attribute| !represented.type || represented.type.passes_attribute_constraint?(attribute, project: represented.project) end diff --git a/modules/backlogs/spec/lib/api/v3/backlog_buckets/backlog_bucket_representer_rendering_spec.rb b/modules/backlogs/spec/lib/api/v3/backlog_buckets/backlog_bucket_representer_rendering_spec.rb new file mode 100644 index 000000000000..9389f2279505 --- /dev/null +++ b/modules/backlogs/spec/lib/api/v3/backlog_buckets/backlog_bucket_representer_rendering_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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" + +RSpec.describe API::V3::BacklogBuckets::BacklogBucketRepresenter, "rendering" do + include API::V3::Utilities::PathHelper + + let(:workspace) { build_stubbed(:project) } + let(:backlog_bucket) { build_stubbed(:backlog_bucket, name: "My Bucket", project: workspace) } + let(:current_user) { build_stubbed(:user) } + let(:embed_links) { true } + let(:representer) { described_class.create(backlog_bucket, current_user:, embed_links:) } + + subject(:generated) { representer.to_json } + + it { is_expected.to include_json("BacklogBucket".to_json).at_path("_type") } + + describe "links" do + it { is_expected.to have_json_type(Object).at_path("_links") } + + describe "self" do + it_behaves_like "has a titled link" do + let(:link) { "self" } + let(:href) { api_v3_paths.backlog_bucket(backlog_bucket.id) } + let(:title) { backlog_bucket.name } + end + end + + describe "definingWorkspace" do + it_behaves_like "has workspace linked" do + let(:link) { "definingWorkspace" } + end + end + end + + describe "properties" do + describe "_type" do + it_behaves_like "property", :_type do + let(:value) { "BacklogBucket" } + end + end + + describe "id" do + it_behaves_like "property", :id do + let(:value) { backlog_bucket.id } + end + end + + describe "name" do + it_behaves_like "property", :name do + let(:value) { backlog_bucket.name } + end + end + + describe "createdAt" do + it_behaves_like "has UTC ISO 8601 date and time" do + let(:date) { backlog_bucket.created_at } + let(:json_path) { "createdAt" } + end + end + + describe "updatedAt" do + it_behaves_like "has UTC ISO 8601 date and time" do + let(:date) { backlog_bucket.updated_at } + let(:json_path) { "updatedAt" } + end + end + end + + describe "embedded" do + it_behaves_like "has workspace embedded" do + let(:embedded_path) { "_embedded/definingWorkspace" } + end + end +end diff --git a/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb b/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb index fab1ec928092..ca81938fddfa 100644 --- a/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb +++ b/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb @@ -161,4 +161,28 @@ end end end + + describe "backlogBucket" do + let(:path) { "backlogBucket" } + + it_behaves_like "has basic schema properties" do + let(:type) { "BacklogBucket" } + let(:name) { I18n.t("activerecord.attributes.work_package.backlog_bucket") } + let(:required) { false } + let(:writable) { true } + let(:location) { "_links" } + end + + it_behaves_like "links to allowed values via collection link" do + let(:href) { "#{api_v3_paths.project_backlog_buckets(project.id)}?filters=%5B%5D&pageSize=-1" } + end + + context "when lacking permission to see the sprints (or if backlogs is disabled)" do + let(:permissions) { %i(view_work_packages edit_work_packages) } + + it "has no reference to the backlog bucket" do + expect(subject).not_to have_json_path(path) + end + end + end end diff --git a/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb b/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb index b6af7bd445bf..fdc2e4382c1a 100644 --- a/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb +++ b/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb @@ -41,7 +41,8 @@ project:, story_points:, position:, - sprint:) + sprint:, + backlog_bucket:) end let(:type) { build_stubbed(:type) } let(:enabled_module_names) { %w[backlogs] } @@ -52,6 +53,7 @@ let(:story_points) { 23 } let(:position) { 123 } let(:sprint) { build_stubbed(:sprint) } + let(:backlog_bucket) { build_stubbed(:backlog_bucket) } let(:embed_links) { true } let(:representer) do described_class.create(work_package, current_user:, embed_links:) @@ -119,6 +121,34 @@ end end + describe "backlogBucket" do + let(:link) { "backlogBucket" } + let(:href) { api_v3_paths.backlog_bucket(backlog_bucket.id) } + let(:title) { backlog_bucket.name } + + it_behaves_like "has a titled link" + + context "when lacking the permission" do + let(:permissions) { [] } + + it_behaves_like "has no link" + end + + context "when the project is empty (because the work package is not persisted yet)" do + let(:project) { nil } + + it_behaves_like "has no link" + end + + context "when the work package has no backlog bucket assigned" do + let(:backlog_bucket) { nil } + + it_behaves_like "has an empty link" do + let(:link) { "backlogBucket" } + end + end + end + describe "update links" do context "when user lacks edit permission but has manage_sprint_items" do let(:permissions) { super() + [:manage_sprint_items] } @@ -150,5 +180,25 @@ it_behaves_like "has the resource not embedded" end end + + describe "backlogBucket" do + let(:embedded_path) { "_embedded/backlogBucket" } + let(:embedded_resource) { backlog_bucket } + let(:embedded_resource_type) { "BacklogBucket" } + + it_behaves_like "has the resource embedded" + + context "when lacking the permission" do + let(:permissions) { [] } + + it_behaves_like "has the resource not embedded" + end + + context "when the work package has no backlog bucket assigned" do + let(:backlog_bucket) { nil } + + it_behaves_like "has the resource not embedded" + end + end end end diff --git a/modules/backlogs/spec/requests/api/v3/backlog_buckets/index_resource_spec.rb b/modules/backlogs/spec/requests/api/v3/backlog_buckets/index_resource_spec.rb new file mode 100644 index 000000000000..a3ccc243e6fd --- /dev/null +++ b/modules/backlogs/spec/requests/api/v3/backlog_buckets/index_resource_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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 "rack/test" + +RSpec.describe "API v3 BacklogBucket resource", content_type: :json do + include Rack::Test::Methods + include API::V3::Utilities::PathHelper + + shared_let(:project) { create(:project, public: false) } + shared_let(:other_project) { create(:project, public: false) } + shared_let(:project_without_permission) { create(:project, public: false) } + + shared_let(:bucket) { create(:backlog_bucket, project:) } + shared_let(:other_bucket) { create(:backlog_bucket, project: other_project) } + shared_let(:bucket_without_permission) { create(:backlog_bucket, project: project_without_permission) } + + let(:permissions) { %i[view_sprints] } + + current_user do + create(:user, + member_with_permissions: { + project => permissions, + other_project => permissions + }) + end + + describe "GET /api/v3/backlog_buckets" do + let(:get_path) { api_v3_paths.path_for(:backlog_buckets, page_size:, offset:) } + let(:page_size) { nil } + let(:offset) { nil } + + before { get get_path } + + context "for a user with view_sprints permission" do + it_behaves_like "API V3 collection response", 2, 2, "BacklogBucket" do + let(:elements) { [other_bucket, bucket] } + end + end + + context "for a user without view_sprints permission" do + let(:permissions) { [] } + + it_behaves_like "API V3 collection response", 0, 0, "BacklogBucket" + end + + context "for an anonymous user" do + let(:current_user) { User.anonymous } + + it_behaves_like "unauthenticated access" + end + + context "with pagination" do + let(:page_size) { 1 } + let(:offset) { 2 } + + it_behaves_like "API V3 collection response", 2, 1, "BacklogBucket" do + let(:elements) { [bucket] } + end + end + end +end diff --git a/modules/backlogs/spec/requests/api/v3/backlog_buckets/project_index_resource_spec.rb b/modules/backlogs/spec/requests/api/v3/backlog_buckets/project_index_resource_spec.rb new file mode 100644 index 000000000000..60a2e3651b06 --- /dev/null +++ b/modules/backlogs/spec/requests/api/v3/backlog_buckets/project_index_resource_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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" + +RSpec.describe "API v3 BacklogBucket resource on project", content_type: :json do + include Rack::Test::Methods + include API::V3::Utilities::PathHelper + + shared_let(:project) { create(:project, public: false) } + shared_let(:other_project) { create(:project, public: false) } + + shared_let(:bucket) { create(:backlog_bucket, project:) } + shared_let(:other_bucket) { create(:backlog_bucket, project: other_project) } + + let(:permissions) { %i[view_sprints] } + + current_user do + create(:user, member_with_permissions: { project => permissions }) + end + + describe "GET /api/v3/projects/:id/backlog_buckets" do + let(:get_path) { api_v3_paths.project_backlog_buckets(project.id) } + + before { get get_path } + + context "for a user with view_sprints permission" do + it_behaves_like "API V3 collection response", 1, 1, "BacklogBucket" do + let(:elements) { [bucket] } + end + end + + context "for a user without view_sprints permission" do + let(:permissions) { [] } + + it_behaves_like "unauthorized access" + end + + context "for a user being not a project member at all" do + let(:get_path) { api_v3_paths.project_backlog_buckets(other_project.id) } + + it_behaves_like "not found" + end + end +end diff --git a/modules/backlogs/spec/requests/api/v3/backlog_buckets/show_resource_spec.rb b/modules/backlogs/spec/requests/api/v3/backlog_buckets/show_resource_spec.rb new file mode 100644 index 000000000000..4b03f2c04b08 --- /dev/null +++ b/modules/backlogs/spec/requests/api/v3/backlog_buckets/show_resource_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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 "rack/test" +require "spec_helper" + +RSpec.describe "API v3 BacklogBucket resource", content_type: :json do + include Rack::Test::Methods + include API::V3::Utilities::PathHelper + + shared_let(:project) { create(:project, public: false) } + shared_let(:bucket) { create(:backlog_bucket, project:) } + + let(:permissions) { %i[view_sprints] } + + current_user do + create(:user, member_with_permissions: { project => permissions }) + end + + describe "GET /api/v3/backlog_buckets/:id" do + let(:get_path) { api_v3_paths.backlog_bucket(bucket.id) } + + before { get get_path } + + context "for a user with view_sprints permission" do + it_behaves_like "successful response", 200, "BacklogBucket" + end + + context "for a user without view_sprints permission" do + let(:permissions) { [] } + + it_behaves_like "not found" + end + + context "for an anonymous user" do + let(:current_user) { User.anonymous } + + it_behaves_like "unauthenticated access" + end + + context "for a bucket that does not exist" do + let(:get_path) { api_v3_paths.backlog_bucket(0) } + + it_behaves_like "not found" + end + end +end From 2bba88c646cd629f353387fbe82ae3a354297ea5 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Thu, 4 Jun 2026 20:42:36 +0200 Subject: [PATCH 02/12] [#73887] Backlog bucket WP details spec --- .../backlog_buckets_on_wp_view_spec.rb | 76 +++++++++++++++++++ spec/support/edit_fields/edit_field.rb | 4 +- 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 modules/backlogs/spec/features/work_packages/backlog_buckets_on_wp_view_spec.rb diff --git a/modules/backlogs/spec/features/work_packages/backlog_buckets_on_wp_view_spec.rb b/modules/backlogs/spec/features/work_packages/backlog_buckets_on_wp_view_spec.rb new file mode 100644 index 000000000000..1f07519b0b31 --- /dev/null +++ b/modules/backlogs/spec/features/work_packages/backlog_buckets_on_wp_view_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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" + +RSpec.describe "Backlog bucket displayed and selectable on work package view", :js do + include Components::Autocompleter::NgSelectAutocompleteHelpers + + shared_let(:project) { create(:project) } + shared_let(:other_project) { create(:project) } + shared_let(:backlog_bucket) { create(:backlog_bucket, project:, name: "Current bucket") } + shared_let(:another_bucket) { create(:backlog_bucket, project:, name: "Another bucket") } + shared_let(:other_project_bucket) { create(:backlog_bucket, project: other_project, name: "Other project bucket") } + shared_let(:work_package) { create(:work_package, project:, backlog_bucket:) } + + let(:permissions) { %i(view_work_packages view_sprints manage_sprint_items) } + let(:wp_page) { Pages::FullWorkPackage.new(work_package) } + + current_user { create(:user, member_with_permissions: { project => permissions }) } + + it "shows the backlog bucket and allows changing it" do + wp_page.visit! + + wp_page.expect_attributes backlog_bucket: backlog_bucket.name + + field = wp_page.work_package_field(:backlog_bucket) + field.activate! + + expect_no_ng_option(field, other_project_bucket.name) + + field.autocomplete(another_bucket.name, select: true) + + wp_page.expect_and_dismiss_toaster message: I18n.t(:notice_successful_update) + + # Ensure the change was persisted: + wp_page.visit! + wp_page.expect_attributes backlog_bucket: another_bucket.name + end + + context "when lacking the permission to see sprints" do + let(:permissions) { %i(view_work_packages) } + + it "does not show a backlog bucket property" do + wp_page.visit! + + wp_page.expect_no_attribute "Backlog bucket" + end + end +end diff --git a/spec/support/edit_fields/edit_field.rb b/spec/support/edit_fields/edit_field.rb index 0fd11b4d7a57..3bd0a551eca4 100644 --- a/spec/support/edit_fields/edit_field.rb +++ b/spec/support/edit_fields/edit_field.rb @@ -33,7 +33,7 @@ def initialize(context, @field_type = derive_field_type @create_form = create_form - @selector = selector || ".inline-edit--container.#{property_name}" + @selector = selector || ".inline-edit--container.#{property_name.to_s.camelize(:lower)}" end def create_form? @@ -327,6 +327,8 @@ def derive_field_type "activity-autocompleter" when :sprint "sprint-autocompleter" + when :backlog_bucket + "backlog-bucket-autocompleter" else "input" end From 780072069a83c6df9de6cffe25b6324bee9f0579 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Fri, 5 Jun 2026 07:51:47 +0200 Subject: [PATCH 03/12] [#73887] Repair WP PDF export spec by introducing buckets --- spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb b/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb index 3e57cfa4c908..17373fc2b530 100644 --- a/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb +++ b/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb @@ -249,6 +249,7 @@ "Details", "Priority", "Normal", *(work_package.sprint.present? ? ["Sprint", work_package.sprint] : ["Sprint"]), + *(work_package.backlog_bucket.present? ? ["Backlog Bucket", work_package.backlog_bucket] : ["Backlog Bucket"]), "Version", work_package.version, "Category", work_package.category, "Project phase", From 318e3596cfe917e805e4b0c76fefd3555c63f5e2 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Mon, 8 Jun 2026 12:39:31 +0200 Subject: [PATCH 04/12] [#73887] Apply Sprint fix from #23523 for Backlog Buckets --- modules/backlogs/lib/open_project/backlogs/engine.rb | 8 +------- .../schema/work_package_schema_representer_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/modules/backlogs/lib/open_project/backlogs/engine.rb b/modules/backlogs/lib/open_project/backlogs/engine.rb index 442274a23393..db276976a9fe 100644 --- a/modules/backlogs/lib/open_project/backlogs/engine.rb +++ b/modules/backlogs/lib/open_project/backlogs/engine.rb @@ -234,16 +234,10 @@ def self.settings end config.to_prepare do - %i[position story_points sprint].each do |attribute| + %i[position story_points sprint backlog_bucket].each do |attribute| ::Type.add_constraint attribute, ->(_type, project: nil) { project.nil? || project.backlogs_enabled? } end - story_and_sprint_permission = ->(_type, project: nil) do - project.nil? || User.current.allowed_in_project?(:view_sprints, project) - end - - ::Type.add_constraint :backlog_bucket, story_and_sprint_permission - ::Type.add_default_mapping(:estimates_and_progress, :story_points) ::Type.add_default_mapping(:other, :position) ::Type.add_default_mapping(:details, :sprint) diff --git a/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb b/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb index ca81938fddfa..b4c0a169b38d 100644 --- a/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb +++ b/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb @@ -44,7 +44,7 @@ allow(p).to receive(:backlogs_enabled?).and_return(backlogs_enabled) end end - let(:work_package_type) { build_stubbed(:type, attribute_groups: [["Agile", %w[position sprint story_points]]]) } + let(:work_package_type) { build_stubbed(:type, attribute_groups: [["Agile", %w[position sprint story_points backlog_bucket]]]) } let(:work_package) { build_stubbed(:work_package, type: work_package_type) } let(:current_user) { build_stubbed(:user) } @@ -138,7 +138,7 @@ describe "attribute_groups" do context "with backlogs enabled" do it "has backlogs properties listed in the right group" do - expect(subject).to be_json_eql(%w[position sprint storyPoints]) + expect(subject).to be_json_eql(%w[position sprint storyPoints backlogBucket]) .at_path("_attributeGroups/0/attributes") end end @@ -147,7 +147,7 @@ let(:permissions) { %i(view_work_packages edit_work_packages) } it "has backlogs properties listed in the right group" do - expect(subject).to be_json_eql(%w[position sprint storyPoints]) + expect(subject).to be_json_eql(%w[position sprint storyPoints backlogBucket]) .at_path("_attributeGroups/0/attributes") end end From 9c93ea845050235c5edca5f4f30ca18494ba32fd Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Wed, 10 Jun 2026 14:47:55 +0200 Subject: [PATCH 05/12] [#73887] mv BacklogBucketQuery -> Query, SprintQuery -> Query --- modules/backlogs/app/models/queries/backlog_buckets.rb | 2 +- .../backlog_buckets/{backlog_bucket_query.rb => query.rb} | 2 +- modules/backlogs/app/models/queries/sprints.rb | 2 +- .../app/models/queries/sprints/{sprint_query.rb => query.rb} | 2 +- ...rint_query_integration_spec.rb => query_integration_spec.rb} | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename modules/backlogs/app/models/queries/backlog_buckets/{backlog_bucket_query.rb => query.rb} (98%) rename modules/backlogs/app/models/queries/sprints/{sprint_query.rb => query.rb} (98%) rename modules/backlogs/spec/models/queries/sprints/{sprint_query_integration_spec.rb => query_integration_spec.rb} (98%) diff --git a/modules/backlogs/app/models/queries/backlog_buckets.rb b/modules/backlogs/app/models/queries/backlog_buckets.rb index 62cf00316f20..b01ee2bf2168 100644 --- a/modules/backlogs/app/models/queries/backlog_buckets.rb +++ b/modules/backlogs/app/models/queries/backlog_buckets.rb @@ -29,7 +29,7 @@ # ++ module Queries::BacklogBuckets - ::Queries::Register.register(BacklogBucketQuery) do + ::Queries::Register.register(Query) do order Orders::DefaultOrder end end diff --git a/modules/backlogs/app/models/queries/backlog_buckets/backlog_bucket_query.rb b/modules/backlogs/app/models/queries/backlog_buckets/query.rb similarity index 98% rename from modules/backlogs/app/models/queries/backlog_buckets/backlog_bucket_query.rb rename to modules/backlogs/app/models/queries/backlog_buckets/query.rb index 33e2f2289db3..c134e744ac04 100644 --- a/modules/backlogs/app/models/queries/backlog_buckets/backlog_bucket_query.rb +++ b/modules/backlogs/app/models/queries/backlog_buckets/query.rb @@ -30,7 +30,7 @@ module Queries module BacklogBuckets - class BacklogBucketQuery + class Query include ::Queries::BaseQuery include ::Queries::UnpersistedQuery diff --git a/modules/backlogs/app/models/queries/sprints.rb b/modules/backlogs/app/models/queries/sprints.rb index 28f66af0e2c5..6da05a142ec8 100644 --- a/modules/backlogs/app/models/queries/sprints.rb +++ b/modules/backlogs/app/models/queries/sprints.rb @@ -29,7 +29,7 @@ # ++ module Queries::Sprints - ::Queries::Register.register(SprintQuery) do + ::Queries::Register.register(Query) do filter Filters::NameFilter filter Filters::ProjectFilter filter Filters::StatusFilter diff --git a/modules/backlogs/app/models/queries/sprints/sprint_query.rb b/modules/backlogs/app/models/queries/sprints/query.rb similarity index 98% rename from modules/backlogs/app/models/queries/sprints/sprint_query.rb rename to modules/backlogs/app/models/queries/sprints/query.rb index 8b81b35b78d0..8a71e1a0bc9c 100644 --- a/modules/backlogs/app/models/queries/sprints/sprint_query.rb +++ b/modules/backlogs/app/models/queries/sprints/query.rb @@ -30,7 +30,7 @@ module Queries module Sprints - class SprintQuery + class Query include ::Queries::BaseQuery include ::Queries::UnpersistedQuery diff --git a/modules/backlogs/spec/models/queries/sprints/sprint_query_integration_spec.rb b/modules/backlogs/spec/models/queries/sprints/query_integration_spec.rb similarity index 98% rename from modules/backlogs/spec/models/queries/sprints/sprint_query_integration_spec.rb rename to modules/backlogs/spec/models/queries/sprints/query_integration_spec.rb index 6e1de23c81ab..9662dad83f37 100644 --- a/modules/backlogs/spec/models/queries/sprints/sprint_query_integration_spec.rb +++ b/modules/backlogs/spec/models/queries/sprints/query_integration_spec.rb @@ -30,7 +30,7 @@ require "spec_helper" -RSpec.describe Queries::Sprints::SprintQuery, "integration" do +RSpec.describe Queries::Sprints::Query, "integration" do shared_let(:project) { create(:project, public: false) } shared_let(:other_project) { create(:project, public: false) } shared_let(:project_without_permission) { create(:project, public: false) } From 2a2e7529abe1cde4d1b32c78489b7b726f9a5e51 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Wed, 10 Jun 2026 15:10:16 +0200 Subject: [PATCH 06/12] [#73887] Remove superfluous project presence checks allowed_in_project? will return false for nil-projects, so this check is not needed. --- .../backlogs/patches/api/work_package_representer.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_representer.rb b/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_representer.rb index 40439ffd5d8f..3dc41979d00d 100644 --- a/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_representer.rb +++ b/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_representer.rb @@ -48,8 +48,7 @@ def extension # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity resource :sprint, link_cache_if: ->(*) { - represented.project.present? && - current_user.allowed_in_project?(:view_sprints, represented.project) + current_user.allowed_in_project?(:view_sprints, represented.project) }, link: ->(*) { if represented.sprint.present? @@ -65,7 +64,6 @@ def extension # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity }, getter: ->(*) do if embed_links && - represented.project.present? && represented.sprint.present? && current_user.allowed_in_project?(:view_sprints, represented.project) ::API::V3::Sprints::SprintRepresenter.create(represented.sprint, current_user:) @@ -75,8 +73,7 @@ def extension # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity resource :backlog_bucket, link_cache_if: ->(*) { - represented.project.present? && - current_user.allowed_in_project?(:view_sprints, represented.project) + current_user.allowed_in_project?(:view_sprints, represented.project) }, link: ->(*) { if represented.backlog_bucket.present? @@ -92,7 +89,6 @@ def extension # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity }, getter: ->(*) do if embed_links && - represented.project.present? && represented.backlog_bucket.present? && current_user.allowed_in_project?(:view_sprints, represented.project) ::API::V3::BacklogBuckets::BacklogBucketRepresenter.create(represented.backlog_bucket, current_user:) From 901e9d33de8fe50c88328f96f8670f407da226ee Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Wed, 10 Jun 2026 15:10:45 +0200 Subject: [PATCH 07/12] [#73887] Align Sprint and Bucket representation path --- .../backlogs/patches/api/work_package_schema_representer.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_schema_representer.rb b/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_schema_representer.rb index bb9f518da85c..6359c6c02bbc 100644 --- a/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_schema_representer.rb +++ b/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_schema_representer.rb @@ -63,8 +63,9 @@ def extension [{ status: { operator: "!", values: [Sprint.statuses["completed"]] } }] )) + query = "filters=#{filters}&pageSize=-1" - "#{api_v3_paths.project_sprints(represented.project_id)}?filters=#{filters}&pageSize=-1" + "#{api_v3_paths.project_sprints(represented.project_id)}?#{query}" } schema_with_allowed_link :backlog_bucket, From 41dd27d8233e07fe078ce3653b3905c7f14c7f57 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Wed, 10 Jun 2026 15:11:35 +0200 Subject: [PATCH 08/12] [#73887] Remove unnecessary cast --- spec/support/edit_fields/edit_field.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/edit_fields/edit_field.rb b/spec/support/edit_fields/edit_field.rb index 3bd0a551eca4..74b66ca2db8b 100644 --- a/spec/support/edit_fields/edit_field.rb +++ b/spec/support/edit_fields/edit_field.rb @@ -33,7 +33,7 @@ def initialize(context, @field_type = derive_field_type @create_form = create_form - @selector = selector || ".inline-edit--container.#{property_name.to_s.camelize(:lower)}" + @selector = selector || ".inline-edit--container.#{@property_name.camelize(:lower)}" end def create_form? From 5e1b00909fb130cfb63e0d72b5f1649a84829adc Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Wed, 10 Jun 2026 15:26:32 +0200 Subject: [PATCH 09/12] [#73887] Revert "[#73887] mv BacklogBucketQuery -> Query, SprintQuery -> Query" This reverts commit 9c93ea845050235c5edca5f4f30ca18494ba32fd. --- modules/backlogs/app/models/queries/backlog_buckets.rb | 2 +- .../backlog_buckets/{query.rb => backlog_bucket_query.rb} | 2 +- modules/backlogs/app/models/queries/sprints.rb | 2 +- .../app/models/queries/sprints/{query.rb => sprint_query.rb} | 2 +- ...ery_integration_spec.rb => sprint_query_integration_spec.rb} | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename modules/backlogs/app/models/queries/backlog_buckets/{query.rb => backlog_bucket_query.rb} (98%) rename modules/backlogs/app/models/queries/sprints/{query.rb => sprint_query.rb} (98%) rename modules/backlogs/spec/models/queries/sprints/{query_integration_spec.rb => sprint_query_integration_spec.rb} (98%) diff --git a/modules/backlogs/app/models/queries/backlog_buckets.rb b/modules/backlogs/app/models/queries/backlog_buckets.rb index b01ee2bf2168..62cf00316f20 100644 --- a/modules/backlogs/app/models/queries/backlog_buckets.rb +++ b/modules/backlogs/app/models/queries/backlog_buckets.rb @@ -29,7 +29,7 @@ # ++ module Queries::BacklogBuckets - ::Queries::Register.register(Query) do + ::Queries::Register.register(BacklogBucketQuery) do order Orders::DefaultOrder end end diff --git a/modules/backlogs/app/models/queries/backlog_buckets/query.rb b/modules/backlogs/app/models/queries/backlog_buckets/backlog_bucket_query.rb similarity index 98% rename from modules/backlogs/app/models/queries/backlog_buckets/query.rb rename to modules/backlogs/app/models/queries/backlog_buckets/backlog_bucket_query.rb index c134e744ac04..33e2f2289db3 100644 --- a/modules/backlogs/app/models/queries/backlog_buckets/query.rb +++ b/modules/backlogs/app/models/queries/backlog_buckets/backlog_bucket_query.rb @@ -30,7 +30,7 @@ module Queries module BacklogBuckets - class Query + class BacklogBucketQuery include ::Queries::BaseQuery include ::Queries::UnpersistedQuery diff --git a/modules/backlogs/app/models/queries/sprints.rb b/modules/backlogs/app/models/queries/sprints.rb index 6da05a142ec8..28f66af0e2c5 100644 --- a/modules/backlogs/app/models/queries/sprints.rb +++ b/modules/backlogs/app/models/queries/sprints.rb @@ -29,7 +29,7 @@ # ++ module Queries::Sprints - ::Queries::Register.register(Query) do + ::Queries::Register.register(SprintQuery) do filter Filters::NameFilter filter Filters::ProjectFilter filter Filters::StatusFilter diff --git a/modules/backlogs/app/models/queries/sprints/query.rb b/modules/backlogs/app/models/queries/sprints/sprint_query.rb similarity index 98% rename from modules/backlogs/app/models/queries/sprints/query.rb rename to modules/backlogs/app/models/queries/sprints/sprint_query.rb index 8a71e1a0bc9c..8b81b35b78d0 100644 --- a/modules/backlogs/app/models/queries/sprints/query.rb +++ b/modules/backlogs/app/models/queries/sprints/sprint_query.rb @@ -30,7 +30,7 @@ module Queries module Sprints - class Query + class SprintQuery include ::Queries::BaseQuery include ::Queries::UnpersistedQuery diff --git a/modules/backlogs/spec/models/queries/sprints/query_integration_spec.rb b/modules/backlogs/spec/models/queries/sprints/sprint_query_integration_spec.rb similarity index 98% rename from modules/backlogs/spec/models/queries/sprints/query_integration_spec.rb rename to modules/backlogs/spec/models/queries/sprints/sprint_query_integration_spec.rb index 9662dad83f37..6e1de23c81ab 100644 --- a/modules/backlogs/spec/models/queries/sprints/query_integration_spec.rb +++ b/modules/backlogs/spec/models/queries/sprints/sprint_query_integration_spec.rb @@ -30,7 +30,7 @@ require "spec_helper" -RSpec.describe Queries::Sprints::Query, "integration" do +RSpec.describe Queries::Sprints::SprintQuery, "integration" do shared_let(:project) { create(:project, public: false) } shared_let(:other_project) { create(:project, public: false) } shared_let(:project_without_permission) { create(:project, public: false) } From a68f3a484d277f66a4d81d78bdf42250615c5b18 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Wed, 10 Jun 2026 15:29:48 +0200 Subject: [PATCH 10/12] [#73887] Reorder wp schema representer spec --- .../work_package_schema_representer_spec.rb | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb b/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb index b4c0a169b38d..4c6d445cda40 100644 --- a/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb +++ b/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb @@ -135,6 +135,42 @@ end end + describe "backlogBucket" do + let(:path) { "backlogBucket" } + + it_behaves_like "has basic schema properties" do + let(:type) { "BacklogBucket" } + let(:name) { I18n.t("activerecord.attributes.work_package.backlog_bucket") } + let(:required) { false } + let(:writable) { true } + let(:location) { "_links" } + end + + it_behaves_like "links to allowed values via collection link" do + let(:href) { "#{api_v3_paths.project_backlog_buckets(project.id)}?filters=%5B%5D&pageSize=-1" } + end + + context "when lacking permission to see the sprints (or if backlogs is disabled)" do + let(:permissions) { %i(view_work_packages edit_work_packages) } + + it "has no reference to the backlog bucket" do + expect(subject).not_to have_json_path(path) + end + end + + context "when lacking permission to set the backlog bucket" do + let(:permissions) { %i(view_work_packages edit_work_packages view_sprints) } + + it_behaves_like "has basic schema properties" do + let(:type) { "BacklogBucket" } + let(:name) { I18n.t("activerecord.attributes.work_package.backlog_bucket") } + let(:required) { false } + let(:writable) { false } + let(:location) { "_links" } + end + end + end + describe "attribute_groups" do context "with backlogs enabled" do it "has backlogs properties listed in the right group" do @@ -161,28 +197,4 @@ end end end - - describe "backlogBucket" do - let(:path) { "backlogBucket" } - - it_behaves_like "has basic schema properties" do - let(:type) { "BacklogBucket" } - let(:name) { I18n.t("activerecord.attributes.work_package.backlog_bucket") } - let(:required) { false } - let(:writable) { true } - let(:location) { "_links" } - end - - it_behaves_like "links to allowed values via collection link" do - let(:href) { "#{api_v3_paths.project_backlog_buckets(project.id)}?filters=%5B%5D&pageSize=-1" } - end - - context "when lacking permission to see the sprints (or if backlogs is disabled)" do - let(:permissions) { %i(view_work_packages edit_work_packages) } - - it "has no reference to the backlog bucket" do - expect(subject).not_to have_json_path(path) - end - end - end end From 98867a4c7307042c61c1fcbef93aaddfe04b021c Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Wed, 10 Jun 2026 15:30:02 +0200 Subject: [PATCH 11/12] [#73887] Add missing test cases for buckets and sprints --- ...work_package_representer_rendering_spec.rb | 22 ++++++++++++++++--- .../v3/sprints/project_index_resource_spec.rb | 6 +++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb b/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb index fdc2e4382c1a..065d0e99dc09 100644 --- a/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb +++ b/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb @@ -67,9 +67,11 @@ current_user { build_stubbed(:user) } before do - mock_permissions_for(current_user) do |mock| - permissions.each do |permission| - mock.allow_in_project(*permission, project:) if project + if project.present? + mock_permissions_for(current_user) do |mock| + permissions.each do |permission| + mock.allow_in_project(*permission, project:) + end end end end @@ -119,6 +121,14 @@ it_behaves_like "has no link" end + + context "when the work package has no backlog bucket assigned" do + let(:sprint) { nil } + + it_behaves_like "has an empty link" do + let(:link) { "sprint" } + end + end end describe "backlogBucket" do @@ -179,6 +189,12 @@ it_behaves_like "has the resource not embedded" end + + context "when the work package has no sprint assigned" do + let(:sprint) { nil } + + it_behaves_like "has the resource not embedded" + end end describe "backlogBucket" do diff --git a/modules/backlogs/spec/requests/api/v3/sprints/project_index_resource_spec.rb b/modules/backlogs/spec/requests/api/v3/sprints/project_index_resource_spec.rb index 2c5d4223a3eb..67786a84acd5 100644 --- a/modules/backlogs/spec/requests/api/v3/sprints/project_index_resource_spec.rb +++ b/modules/backlogs/spec/requests/api/v3/sprints/project_index_resource_spec.rb @@ -71,5 +71,11 @@ it_behaves_like "unauthorized access" end + + context "for a user being not a project member at all" do + let(:get_path) { api_v3_paths.project_sprints(project_without_permission.id) } + + it_behaves_like "not found" + end end end From 735bba8d250f7e6ae870a77e92fa25429b110300 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Wed, 10 Jun 2026 15:43:42 +0200 Subject: [PATCH 12/12] [#73887] mocked_permission_helper mirrors UserPermissibleService for empty project It should not grant all permissions if the project argument is `nil`. --- .../work_package_representer_rendering_spec.rb | 8 +++----- spec/support/mocked_permission_helper.rb | 2 +- spec/support_spec/mocked_permission_helper_spec.rb | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb b/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb index 065d0e99dc09..93aa1c13b9f1 100644 --- a/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb +++ b/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb @@ -67,11 +67,9 @@ current_user { build_stubbed(:user) } before do - if project.present? - mock_permissions_for(current_user) do |mock| - permissions.each do |permission| - mock.allow_in_project(*permission, project:) - end + mock_permissions_for(current_user) do |mock| + permissions.each do |permission| + mock.allow_in_project(*permission, project:) if project end end end diff --git a/spec/support/mocked_permission_helper.rb b/spec/support/mocked_permission_helper.rb index 47bf733caf7d..b1a13adc8402 100644 --- a/spec/support/mocked_permission_helper.rb +++ b/spec/support/mocked_permission_helper.rb @@ -135,7 +135,7 @@ def mock_permissions_for(user) # rubocop:disable Metrics/PerceivedComplexity next true if permission_mock.admin_allowed_to?(permissions) permission_names = permissions.map(&:name) - projects.all? do |project| + projects.any? && projects.all? do |project| permission_mock.permitted_entities[project].intersect?(permission_names) end end diff --git a/spec/support_spec/mocked_permission_helper_spec.rb b/spec/support_spec/mocked_permission_helper_spec.rb index cd8f2b5a871a..12603e67f739 100644 --- a/spec/support_spec/mocked_permission_helper_spec.rb +++ b/spec/support_spec/mocked_permission_helper_spec.rb @@ -187,7 +187,7 @@ end it "allows the project permission when querying with controller and action hash" do - expect(user).to be_allowed_in_project({ controller: "work_packages", action: "index", project_id: project.id }, nil) + expect(user).to be_allowed_in_project({ controller: "work_packages", action: "index", project_id: project.id }, project) expect(user).to be_allowed_in_any_project({ controller: "work_packages", action: "index" }) end