Merge branch 'release/17.4' into release/17.5

This commit is contained in:
OpenProject Actions CI
2026-05-26 05:10:56 +00:00
5 changed files with 137 additions and 2 deletions
+1
View File
@@ -6270,6 +6270,7 @@ en:
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.
workPackage: Undisclosed - The work package is invisible because of lacking permissions.
doorkeeper:
pre_authorization:
+41
View File
@@ -193,6 +193,47 @@ module API
skip_render:)
end
# Like associated_resource, but skips rendering and shows an undisclosed
# link when the associated record exists but is not visible to the current user.
# Requires the associated model to implement +visible?(user)+.
def associated_visible_resource(name,
as: nil,
representer: nil,
v3_path: name,
link_title_attribute: :name,
undisclosed_title: :"api_v3.undisclosed.#{name.to_s.camelize(:lower)}")
associated_resource(
name,
as:,
representer:,
v3_path:,
skip_render: ->(*) {
represented.public_send(:"#{name}_id").nil? ||
!represented.public_send(name)&.visible?(current_user)
},
link: associated_visible_resource_link_lambda(name,
v3_path:,
link_title_attribute:,
undisclosed_title:)
)
end
def associated_visible_resource_link_lambda(name, v3_path:, link_title_attribute:, undisclosed_title:)
->(*) do
id = represented.public_send(:"#{name}_id")
next if id.nil?
resource = represented.public_send(name)
if resource&.visible?(current_user)
{ href: api_v3_paths.public_send(v3_path, id),
title: resource.public_send(link_title_attribute) }
else
{ href: ::API::V3::URN_UNDISCLOSED,
title: I18n.t(undisclosed_title) }
end
end
end
def link_attr(name, uncacheable, link_cache_if)
links_attr = { rel: name.to_s.camelize(:lower) }
links_attr[:uncacheable] = true if uncacheable
@@ -77,8 +77,7 @@ module API
representer: ::API::V3::Users::UserRepresenter,
skip_render: ->(*) { represented.presenter_id.nil? }
associated_resource :work_package,
skip_render: ->(*) { represented.work_package_id.nil? }
associated_visible_resource :work_package
associated_resource :meeting_section,
as: :section,
@@ -138,6 +138,30 @@ RSpec.describe "API v3 Meeting Agenda Items sub-resource", content_type: :json d
expect(last_response).to have_http_status(:not_found)
end
end
context "when the agenda item is linked to a work package in an inaccessible project" do
let(:private_project) { create(:project, public: false) }
let(:private_work_package) { create(:work_package, project: private_project) }
let!(:wp_agenda_item) do
create(:wp_meeting_agenda_item, meeting:, meeting_section: section, work_package: private_work_package,
author: current_user)
end
let(:path) { api_v3_paths.meeting_agenda_item(meeting.id, wp_agenda_item.id) }
it "returns 200" do
expect(last_response).to have_http_status(:ok)
end
it "does not embed the inaccessible work package" do
expect(last_response.body).not_to have_json_path("_embedded/workPackage")
end
it "renders the work package link as undisclosed" do
expect(last_response.body)
.to be_json_eql(::API::V3::URN_UNDISCLOSED.to_json)
.at_path("_links/workPackage/href")
end
end
end
describe "PATCH /api/v3/meetings/:meeting_id/agenda_items/:id" do
@@ -43,6 +43,76 @@ RSpec.describe API::Decorators::LinkedResource do
let(:represented) { {} }
let(:current_user) { create(:user) }
describe ".associated_visible_resource" do
include API::V3::Utilities::PathHelper
let(:thing_representer_class) do
Class.new(API::Decorators::Single) do
property :id
def _type = "Thing"
end
end
let(:representer_class) do
klass = thing_representer_class
Class.new(API::Decorators::Single) do
include API::Decorators::LinkedResource
associated_visible_resource :thing, v3_path: :thing, representer: klass,
undisclosed_title: :"api_v3.undisclosed.parent"
end
end
let(:thing_id) { 42 }
let(:thing) { double("thing", id: thing_id, name: "Thing Name") }
let(:model) { Struct.new(:thing_id, :thing).new(thing_id, thing) }
before do
without_partial_double_verification do
allow(api_v3_paths).to receive(:thing).with(thing_id).and_return("/api/v3/things/#{thing_id}")
end
end
subject(:json) { representer_class.new(model, current_user:, embed_links: true).to_json }
context "when the resource is visible" do
before { allow(thing).to receive(:visible?).with(current_user).and_return(true) }
it "renders the link href and title" do
expect(json).to be_json_eql("/api/v3/things/42".to_json).at_path("_links/thing/href")
expect(json).to be_json_eql("Thing Name".to_json).at_path("_links/thing/title")
end
it "embeds the resource" do
expect(json).to have_json_path("_embedded/thing")
end
end
context "when the resource is not visible" do
before { allow(thing).to receive(:visible?).with(current_user).and_return(false) }
it "renders the link href as undisclosed" do
expect(json).to be_json_eql(::API::V3::URN_UNDISCLOSED.to_json).at_path("_links/thing/href")
end
it "does not embed the resource" do
expect(json).not_to have_json_path("_embedded/thing")
end
end
context "when the resource id is nil" do
let(:thing_id) { nil }
let(:thing) { nil }
it "renders no link" do
expect(json).not_to have_json_path("_links/thing")
end
it "does not embed the resource" do
expect(json).not_to have_json_path("_embedded/thing")
end
end
end
describe "#from_hash" do
subject { representer.new(represented, current_user:).from_hash(input_hash) }