From 993b3e53f07af7afbd41e83f9cf62425d23cf53c Mon Sep 17 00:00:00 2001 From: ulferts Date: Thu, 26 Feb 2026 17:57:43 +0100 Subject: [PATCH] add a sprint GET end point to v3 --- config/locales/en.yml | 1 + lib/api/v3/workspaces/linked_resource.rb | 2 +- modules/backlogs/app/models/agile/sprint.rb | 4 + .../models/agile/sprints/scopes/visible.rb | 45 +++++ modules/backlogs/config/locales/en.yml | 4 + .../lib/api/v3/sprints/sprint_representer.rb | 72 ++++++++ .../lib/api/v3/sprints/sprints_api.rb | 49 ++++++ .../lib/open_project/backlogs/engine.rb | 8 + .../sprint_representer_rendering_spec.rb | 156 ++++++++++++++++++ .../agile/sprints/scopes/visible_spec.rb | 105 ++++++++++++ .../api/v3/sprints/show_resource_spec.rb | 80 +++++++++ 11 files changed, 525 insertions(+), 1 deletion(-) create mode 100644 modules/backlogs/app/models/agile/sprints/scopes/visible.rb create mode 100644 modules/backlogs/lib/api/v3/sprints/sprint_representer.rb create mode 100644 modules/backlogs/lib/api/v3/sprints/sprints_api.rb create mode 100644 modules/backlogs/spec/lib/api/v3/sprints/sprint_representer_rendering_spec.rb create mode 100644 modules/backlogs/spec/models/agile/sprints/scopes/visible_spec.rb create mode 100644 modules/backlogs/spec/requests/api/v3/sprints/show_resource_spec.rb diff --git a/config/locales/en.yml b/config/locales/en.yml index 2d789e00cc8..b219cf61e6f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -5706,6 +5706,7 @@ en: project: Undisclosed - The project is invisible because of lacking permissions. ancestor: Undisclosed - The ancestor is invisible because of lacking permissions. definingProject: Undisclosed - The project is invisible because of lacking permissions. + definingWorkspace: Undisclosed - The workspace is invisible because of lacking permissions. doorkeeper: pre_authorization: diff --git a/lib/api/v3/workspaces/linked_resource.rb b/lib/api/v3/workspaces/linked_resource.rb index 6a22481c8aa..17613907628 100644 --- a/lib/api/v3/workspaces/linked_resource.rb +++ b/lib/api/v3/workspaces/linked_resource.rb @@ -70,7 +70,7 @@ module API representer: ::API::V3::Projects::ProjectRepresenter, skip_render:, link: ::API::V3::Workspaces::WorkspaceRepresenterFactory - .create_link_lambda(name), + .create_link_lambda(name, property_name: as), setter: ::API::V3::Workspaces::WorkspaceRepresenterFactory .create_setter_lambda(name) } diff --git a/modules/backlogs/app/models/agile/sprint.rb b/modules/backlogs/app/models/agile/sprint.rb index 8905ffd709d..f757080dd0d 100644 --- a/modules/backlogs/app/models/agile/sprint.rb +++ b/modules/backlogs/app/models/agile/sprint.rb @@ -76,6 +76,10 @@ module Agile validate :validate_only_one_active_sprint_per_project + include ::Scopes::Scoped + + scopes :visible + # TODO: validate sharing is set to an allowed value, e.g. only admins may share systemwide (#71374, #71253) # TODO: implement sharing logic once it has been defined (#71374) diff --git a/modules/backlogs/app/models/agile/sprints/scopes/visible.rb b/modules/backlogs/app/models/agile/sprints/scopes/visible.rb new file mode 100644 index 00000000000..cc3ee1385b2 --- /dev/null +++ b/modules/backlogs/app/models/agile/sprints/scopes/visible.rb @@ -0,0 +1,45 @@ +# 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 Agile::Sprints::Scopes + module Visible + extend ActiveSupport::Concern + + class_methods do + # Returns all sprints the user is allowed to see. + # A sprint is visible if the user has the :view_sprints permission + # in the project the sprint belongs to. + def visible(user = User.current) + joins(:project) + .merge(Project.allowed_to(user, :view_sprints)) + end + end + end +end diff --git a/modules/backlogs/config/locales/en.yml b/modules/backlogs/config/locales/en.yml index 3d26e704927..a5ffa720469 100644 --- a/modules/backlogs/config/locales/en.yml +++ b/modules/backlogs/config/locales/en.yml @@ -40,6 +40,10 @@ en: goal: "Sprint goal" name: "Sprint name" sharing: "Sharing" + statuses: + in_planning: "In planning" + active: "Active" + completed: "Completed" sprint: duration: "Sprint duration" work_package: diff --git a/modules/backlogs/lib/api/v3/sprints/sprint_representer.rb b/modules/backlogs/lib/api/v3/sprints/sprint_representer.rb new file mode 100644 index 00000000000..0cd185cbb6f --- /dev/null +++ b/modules/backlogs/lib/api/v3/sprints/sprint_representer.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 "roar/decorator" +require "roar/json/hal" + +module API + module V3 + module Sprints + class SprintRepresenter < ::API::Decorators::Single + include API::Decorators::LinkedResource + include API::V3::Workspaces::LinkedResource + include API::Decorators::DateProperty + + self_link + + link :status do + { + href: "#{::API::V3::URN_PREFIX}sprints:status:#{represented.status}", + title: I18n.t("activerecord.attributes.agile/sprint.statuses.#{represented.status}") + } + end + + associated_project as: :definingWorkspace + + property :id, + render_nil: true + + property :name, + render_nil: true + + date_property :start_date + + date_property :finish_date + + date_time_property :created_at + date_time_property :updated_at + + def _type + "Sprint" + end + end + end + end +end diff --git a/modules/backlogs/lib/api/v3/sprints/sprints_api.rb b/modules/backlogs/lib/api/v3/sprints/sprints_api.rb new file mode 100644 index 00000000000..f79213a4aa5 --- /dev/null +++ b/modules/backlogs/lib/api/v3/sprints/sprints_api.rb @@ -0,0 +1,49 @@ +# 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 Sprints + class SprintsAPI < ::API::OpenProjectAPI + resources :sprints do + route_param :id, type: Integer, desc: "Sprint ID" do + after_validation do + guard_feature_flag(:scrum_projects) + + @sprint = Agile::Sprint.visible(current_user).find(params[:id]) + end + + get &::API::V3::Utilities::Endpoints::Show.new(model: Agile::Sprint).mount + end + 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 f259da89eeb..e3b80d3333e 100644 --- a/modules/backlogs/lib/open_project/backlogs/engine.rb +++ b/modules/backlogs/lib/open_project/backlogs/engine.rb @@ -180,6 +180,14 @@ module OpenProject::Backlogs "#{root}/backlogs_types/#{id}" end + add_api_path :sprint do |id| + "#{root}/sprints/#{id}" + end + + add_api_endpoint "API::V3::Root" do + mount ::API::V3::Sprints::SprintsAPI + end + config.to_prepare do OpenProject::Backlogs::Hooks::LayoutHook OpenProject::Backlogs::Hooks::UserSettingsHook diff --git a/modules/backlogs/spec/lib/api/v3/sprints/sprint_representer_rendering_spec.rb b/modules/backlogs/spec/lib/api/v3/sprints/sprint_representer_rendering_spec.rb new file mode 100644 index 00000000000..155a6911273 --- /dev/null +++ b/modules/backlogs/spec/lib/api/v3/sprints/sprint_representer_rendering_spec.rb @@ -0,0 +1,156 @@ +# 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::Sprints::SprintRepresenter, "rendering" do + include API::V3::Utilities::PathHelper + + let(:workspace) { build_stubbed(:project) } + let(:start_date) { Date.new(2024, 1, 1) } + let(:finish_date) { Date.new(2024, 1, 10) } + let(:status) { "in_planning" } + let(:sprint) do + build_stubbed(:agile_sprint, + project: workspace, + status:, + name: "Sprint 1", + start_date:, + finish_date:) + end + let(:current_user) { build_stubbed(:user) } + let(:embed_links) { true } + let(:representer) { described_class.create(sprint, current_user:, embed_links:) } + + subject(:generated) { representer.to_json } + + it { is_expected.to include_json("Sprint".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.sprint(sprint.id) } + let(:title) { sprint.name } + end + end + + describe "definingWorkspace" do + it_behaves_like "has workspace linked" do + let(:link) { "definingWorkspace" } + end + end + + describe "status" do + let(:link) { "status" } + + context "with in_planning value" do + it_behaves_like "has a titled link" do + let(:href) { "urn:openproject-org:api:v3:sprints:status:in_planning" } + let(:title) { I18n.t("activerecord.attributes.agile/sprint.statuses.in_planning") } + end + end + + context "with active value" do + let(:status) { "active" } + + it_behaves_like "has a titled link" do + let(:href) { "urn:openproject-org:api:v3:sprints:status:active" } + let(:title) { I18n.t("activerecord.attributes.agile/sprint.statuses.active") } + end + end + + context "with completed value" do + let(:status) { "completed" } + + it_behaves_like "has a titled link" do + let(:href) { "urn:openproject-org:api:v3:sprints:status:completed" } + let(:title) { I18n.t("activerecord.attributes.agile/sprint.statuses.completed") } + end + end + end + end + + describe "properties" do + describe "_type" do + it_behaves_like "property", :_type do + let(:value) { "Sprint" } + end + end + + describe "id" do + it_behaves_like "property", :id do + let(:value) { sprint.id } + end + end + + describe "name" do + it_behaves_like "property", :name do + let(:value) { sprint.name } + end + end + + describe "startDate" do + it_behaves_like "has ISO 8601 date only" do + let(:date) { start_date } + let(:json_path) { "startDate" } + end + end + + describe "finishDate" do + it_behaves_like "has ISO 8601 date only" do + let(:date) { finish_date } + let(:json_path) { "finishDate" } + end + end + + describe "createdAt" do + it_behaves_like "has UTC ISO 8601 date and time" do + let(:date) { sprint.created_at } + let(:json_path) { "createdAt" } + end + end + + describe "updatedAt" do + it_behaves_like "has UTC ISO 8601 date and time" do + let(:date) { sprint.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/models/agile/sprints/scopes/visible_spec.rb b/modules/backlogs/spec/models/agile/sprints/scopes/visible_spec.rb new file mode 100644 index 00000000000..29ac3450c4e --- /dev/null +++ b/modules/backlogs/spec/models/agile/sprints/scopes/visible_spec.rb @@ -0,0 +1,105 @@ +# 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 Agile::Sprints::Scopes::Visible do + shared_let(:project) { create(:project) } + shared_let(:other_project) { create(:project) } + shared_let(:sprint) { create(:agile_sprint, project:) } + shared_let(:sprint_in_other_project) { create(:agile_sprint, project: other_project) } + shared_let(:role) { create(:project_role, permissions: [:view_sprints]) } + shared_let(:user_with_permission) do + create(:user).tap do |u| + create(:member, project:, user: u, roles: [role]) + end + end + shared_let(:user_with_permission_in_both) do + create(:user).tap do |u| + create(:member, project:, user: u, roles: [role]) + create(:member, project: other_project, user: u, roles: [role]) + end + end + shared_let(:user_without_permission) do + create(:user).tap do |u| + create(:member, + project:, + user: u, + roles: [create(:project_role, permissions: [:view_work_packages])]) + end + end + shared_let(:user_without_membership) { create(:user) } + + subject { Agile::Sprint.visible(current_user) } + + context "for a user with view_sprints in one project" do + current_user { user_with_permission } + + it "returns the sprint in that project" do + expect(subject).to contain_exactly(sprint) + end + + it "does not return sprints from projects the user has no permission in" do + expect(subject).not_to include(sprint_in_other_project) + end + end + + context "for a user with view_sprnts in both projects" do + current_user { user_with_permission_in_both } + + it "returns sprints from both projects" do + expect(subject).to contain_exactly(sprint, sprint_in_other_project) + end + end + + context "for a user with a different permission but not view_sprints" do + current_user { user_without_permission } + + it "returns no sprints" do + expect(subject).to be_empty + end + end + + context "for a user without any membership" do + current_user { user_without_membership } + + it "returns no sprints" do + expect(subject).to be_empty + end + end + + context "when called without a user argument" do + current_user { user_with_permission } + + it "uses User.current" do + expect(Agile::Sprint.visible).to contain_exactly(sprint) + end + end +end diff --git a/modules/backlogs/spec/requests/api/v3/sprints/show_resource_spec.rb b/modules/backlogs/spec/requests/api/v3/sprints/show_resource_spec.rb new file mode 100644 index 00000000000..40e68c0fb22 --- /dev/null +++ b/modules/backlogs/spec/requests/api/v3/sprints/show_resource_spec.rb @@ -0,0 +1,80 @@ +# 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 Sprint resource", content_type: :json do + include Rack::Test::Methods + include API::V3::Utilities::PathHelper + + shared_let(:project) { create(:project, public: false) } + shared_let(:sprint) { create(:agile_sprint, project:) } + + let(:permissions) { %i[view_sprints] } + + current_user do + create(:user, member_with_permissions: { project => permissions }) + end + + describe "GET /api/v3/sprints/:id", with_flag: :scrum_projects do + let(:get_path) { api_v3_paths.sprint(sprint.id) } + + before do + get get_path + end + + context "for a user with view_sprints permission" do + it_behaves_like "successful response", 200, "Sprint" + 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 sprint that does not exist" do + let(:get_path) { api_v3_paths.sprint(0) } + + it_behaves_like "not found" + end + + context "when the feature flag is turned off", with_flag: { scrum_projects: false } do + it_behaves_like "not found" + end + end +end