mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Fix meeting backlog section exposing API
This commit is contained in:
@@ -23,6 +23,10 @@ properties:
|
||||
position:
|
||||
type: integer
|
||||
description: The position of the section within the meeting.
|
||||
backlog:
|
||||
type: boolean
|
||||
description: |-
|
||||
Whether this section is the meeting's backlog. Backlog sections are read-only via the API.
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
@@ -5,7 +5,12 @@ get:
|
||||
operationId: get_meeting_section
|
||||
tags:
|
||||
- Meetings
|
||||
description: Retrieve an individual meeting section.
|
||||
description: |-
|
||||
Retrieve an individual meeting section.
|
||||
|
||||
The backlog section can be retrieved via this endpoint even though it is not part of the regular
|
||||
`meeting.sections` association. Its `_links.meeting` points to the owning meeting (the series
|
||||
template for recurring meeting occurrences).
|
||||
parameters:
|
||||
- description: Section identifier
|
||||
example: 1
|
||||
@@ -34,7 +39,10 @@ patch:
|
||||
operationId: update_meeting_section
|
||||
tags:
|
||||
- Meetings
|
||||
description: Updates the given meeting section.
|
||||
description: |-
|
||||
Updates the given meeting section.
|
||||
|
||||
The backlog section cannot be updated and will return `422 Unprocessable Entity`.
|
||||
parameters:
|
||||
- description: Section identifier
|
||||
example: 1
|
||||
@@ -88,7 +96,10 @@ delete:
|
||||
operationId: delete_meeting_section
|
||||
tags:
|
||||
- Meetings
|
||||
description: Deletes the meeting section.
|
||||
description: |-
|
||||
Deletes the meeting section and all its agenda items.
|
||||
|
||||
The backlog section cannot be deleted and will return `422 Unprocessable Entity`.
|
||||
parameters:
|
||||
- description: Section identifier
|
||||
example: 1
|
||||
|
||||
@@ -5,7 +5,13 @@ get:
|
||||
operationId: get_meeting_section_by_meeting
|
||||
tags:
|
||||
- Meetings
|
||||
description: Retrieve an individual section of a meeting.
|
||||
description: |-
|
||||
Retrieve an individual section of a meeting.
|
||||
|
||||
The backlog section can be retrieved via this nested endpoint even though it is not part of the
|
||||
regular `meeting.sections` association. Its `_links.self` and `_links.meeting` point to the
|
||||
canonical flat section resource and the owning meeting (the series template for recurring
|
||||
meeting occurrences).
|
||||
parameters:
|
||||
- description: Meeting identifier
|
||||
example: 1
|
||||
|
||||
@@ -5,7 +5,11 @@ get:
|
||||
operationId: list_meeting_sections
|
||||
tags:
|
||||
- Meetings
|
||||
description: Lists all sections for the given meeting.
|
||||
description: |-
|
||||
Lists all sections for the given meeting, including the backlog section as the last element.
|
||||
|
||||
The backlog section is read-only and its `_links.meeting` points to the owning meeting (the
|
||||
series template for recurring meeting occurrences, otherwise the meeting itself).
|
||||
parameters:
|
||||
- description: Meeting identifier
|
||||
example: 1
|
||||
|
||||
@@ -32,5 +32,11 @@ module MeetingSections
|
||||
include EditableItem
|
||||
|
||||
delete_permission :manage_agendas
|
||||
|
||||
validate :backlog_not_deletable
|
||||
|
||||
def backlog_not_deletable
|
||||
errors.add :base, :error_readonly if model.backlog?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
module MeetingSections
|
||||
class UpdateContract < BaseContract
|
||||
validate :user_allowed_to_edit
|
||||
validate :backlog_not_editable
|
||||
|
||||
# We allow an empty title internally via create to mark an untitled/implicit section
|
||||
# but users should not be able to update it with an empty title through this contract
|
||||
@@ -44,5 +45,9 @@ module MeetingSections
|
||||
errors.add :base, :error_unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
def backlog_not_editable
|
||||
errors.add :base, :error_readonly if model.backlog?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,6 +46,8 @@ module API
|
||||
|
||||
property :position
|
||||
|
||||
property :backlog
|
||||
|
||||
associated_resource :meeting,
|
||||
link: ->(*) {
|
||||
{
|
||||
|
||||
@@ -32,9 +32,17 @@ module API
|
||||
module V3
|
||||
module MeetingSections
|
||||
class SectionsByMeetingAPI < ::API::OpenProjectAPI
|
||||
helpers do
|
||||
def find_backlog_section(id)
|
||||
backlog = @meeting.backlog
|
||||
backlog if backlog&.id == id
|
||||
end
|
||||
end
|
||||
|
||||
resources :sections do
|
||||
get do
|
||||
sections = @meeting.sections
|
||||
sections = @meeting.sections.to_a
|
||||
sections << @meeting.backlog if @meeting.backlog.present?
|
||||
MeetingSectionCollectionRepresenter.new(sections,
|
||||
self_link: api_v3_paths.meeting_sections(meeting_id: @meeting.id),
|
||||
current_user:)
|
||||
@@ -42,7 +50,9 @@ module API
|
||||
|
||||
route_param :section_id, type: Integer, desc: "Section ID" do
|
||||
after_validation do
|
||||
@meeting_section = @meeting.sections.find(declared_params[:section_id])
|
||||
@meeting_section = @meeting.sections.find_by(id: declared_params[:section_id]) ||
|
||||
find_backlog_section(declared_params[:section_id])
|
||||
raise API::Errors::NotFound.new unless @meeting_section
|
||||
end
|
||||
|
||||
get &::API::V3::Utilities::Endpoints::Show.new(model: MeetingSection).mount
|
||||
|
||||
@@ -62,6 +62,12 @@ RSpec.describe MeetingSections::DeleteContract do
|
||||
|
||||
it_behaves_like "contract is valid"
|
||||
end
|
||||
|
||||
context "when the section is the backlog" do
|
||||
let(:section) { meeting.backlog }
|
||||
|
||||
it_behaves_like "contract is invalid", base: :error_readonly
|
||||
end
|
||||
end
|
||||
|
||||
context "without permission" do
|
||||
|
||||
@@ -54,6 +54,12 @@ RSpec.describe MeetingSections::UpdateContract do
|
||||
|
||||
it_behaves_like "contract is invalid", base: I18n.t(:text_agenda_item_not_editable_anymore)
|
||||
end
|
||||
|
||||
context "when the section is the backlog" do
|
||||
let(:section) { meeting.backlog }
|
||||
|
||||
it_behaves_like "contract is invalid", base: :error_readonly
|
||||
end
|
||||
end
|
||||
|
||||
context "without permission" do
|
||||
|
||||
+36
@@ -203,6 +203,42 @@ RSpec.describe "API v3 Meeting Agenda Items sub-resource", content_type: :json d
|
||||
end
|
||||
end
|
||||
|
||||
context "when the agenda item is part of the backlog" do
|
||||
let(:backlog) { meeting.backlog }
|
||||
let!(:backlog_agenda_item) do
|
||||
create(:meeting_agenda_item,
|
||||
meeting:,
|
||||
meeting_section: backlog,
|
||||
author: current_user,
|
||||
title: "Backlog agenda item")
|
||||
end
|
||||
let(:path) { api_v3_paths.meeting_agenda_item(backlog_agenda_item.id, meeting_id: meeting.id) }
|
||||
|
||||
it "renders the backlog section correctly" do
|
||||
expect(last_response).to have_http_status(:ok)
|
||||
|
||||
expect(last_response.body)
|
||||
.to be_json_eql(api_v3_paths.meeting_section(backlog.id).to_json)
|
||||
.at_path("_links/section/href")
|
||||
|
||||
expect(last_response.body)
|
||||
.to be_json_eql(backlog.title.to_json)
|
||||
.at_path("_links/section/title")
|
||||
|
||||
expect(last_response.body)
|
||||
.to be_json_eql(backlog.id.to_json)
|
||||
.at_path("_embedded/section/id")
|
||||
|
||||
expect(last_response.body)
|
||||
.to be_json_eql(true.to_json)
|
||||
.at_path("_embedded/section/backlog")
|
||||
|
||||
expect(last_response.body)
|
||||
.to be_json_eql(api_v3_paths.meeting(meeting.id).to_json)
|
||||
.at_path("_embedded/section/_links/meeting/href")
|
||||
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) }
|
||||
|
||||
+132
@@ -250,6 +250,138 @@ RSpec.describe "API v3 Meeting Sections sub-resource", content_type: :json do
|
||||
end
|
||||
end
|
||||
|
||||
describe "backlog section" do
|
||||
let(:backlog) { meeting.backlog }
|
||||
|
||||
describe "GET /api/v3/meetings/:meeting_id/sections" do
|
||||
let(:path) { api_v3_paths.meeting_sections(meeting_id: meeting.id) }
|
||||
|
||||
before { get path }
|
||||
|
||||
it "includes the backlog as the last section" do
|
||||
elements = JSON.parse(last_response.body).dig("_embedded", "elements")
|
||||
|
||||
expect(elements.pluck("id")).to eq([section.id, backlog.id])
|
||||
expect(elements.last).to include("backlog" => true)
|
||||
expect(elements.first).to include("backlog" => false)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/v3/meetings/:meeting_id/sections/:id" do
|
||||
let(:path) { api_v3_paths.meeting_section(backlog.id, meeting_id: meeting.id) }
|
||||
|
||||
before { get path }
|
||||
|
||||
it "returns the backlog with canonical links" do
|
||||
expect(last_response).to have_http_status(:ok)
|
||||
|
||||
expect(last_response.body)
|
||||
.to be_json_eql(backlog.id.to_json)
|
||||
.at_path("id")
|
||||
|
||||
expect(last_response.body)
|
||||
.to be_json_eql(true.to_json)
|
||||
.at_path("backlog")
|
||||
|
||||
expect(last_response.body)
|
||||
.to be_json_eql(api_v3_paths.meeting_section(backlog.id).to_json)
|
||||
.at_path("_links/self/href")
|
||||
|
||||
expect(last_response.body)
|
||||
.to be_json_eql(api_v3_paths.meeting(meeting.id).to_json)
|
||||
.at_path("_links/meeting/href")
|
||||
end
|
||||
end
|
||||
|
||||
describe "PATCH /api/v3/meeting_sections/:id" do
|
||||
let(:path) { api_v3_paths.meeting_section(backlog.id) }
|
||||
let(:original_title) { backlog.title }
|
||||
let(:body) { { title: "Updated Backlog Title" }.to_json }
|
||||
|
||||
subject(:response) { patch path, body }
|
||||
|
||||
it "responds with 422 and does not change the title" do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(backlog.reload.title).to eq(original_title)
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /api/v3/meeting_sections/:id" do
|
||||
let(:path) { api_v3_paths.meeting_section(backlog.id) }
|
||||
|
||||
before { delete path }
|
||||
|
||||
it "responds with 422 and keeps the backlog" do
|
||||
expect(last_response).to have_http_status(:unprocessable_entity)
|
||||
expect(MeetingSection).to exist(backlog.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "backlog section for a recurring meeting occurrence" do
|
||||
let(:recurring_meeting) { create(:recurring_meeting, project:, author: current_user) }
|
||||
let(:template) { recurring_meeting.template }
|
||||
let(:occurrence) do
|
||||
create(:recurring_meeting_occurrence, recurring_meeting:, project:, author: current_user)
|
||||
end
|
||||
let!(:occurrence_section) { create(:meeting_section, meeting: occurrence, title: "Occurrence Section") }
|
||||
let(:backlog) { template.backlog }
|
||||
|
||||
describe "GET /api/v3/meetings/:meeting_id/sections" do
|
||||
let(:path) { api_v3_paths.meeting_sections(meeting_id: occurrence.id) }
|
||||
|
||||
before { get path }
|
||||
|
||||
it "includes the backlog with canonical links to the template meeting" do
|
||||
elements = JSON.parse(last_response.body).dig("_embedded", "elements")
|
||||
backlog_element = elements.find { |element| element["id"] == backlog.id }
|
||||
|
||||
expect(elements.pluck("id")).to eq([occurrence_section.id, backlog.id])
|
||||
expect(backlog_element).to include("backlog" => true)
|
||||
expect(backlog_element.dig("_links", "self", "href"))
|
||||
.to eq(api_v3_paths.meeting_section(backlog.id))
|
||||
expect(backlog_element.dig("_links", "meeting", "href"))
|
||||
.to eq(api_v3_paths.meeting(template.id))
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/v3/meetings/:meeting_id/sections/:id" do
|
||||
let(:path) { api_v3_paths.meeting_section(backlog.id, meeting_id: occurrence.id) }
|
||||
|
||||
before { get path }
|
||||
|
||||
it "returns the backlog with canonical links to the template meeting" do
|
||||
expect(last_response).to have_http_status(:ok)
|
||||
|
||||
expect(last_response.body)
|
||||
.to be_json_eql(api_v3_paths.meeting_section(backlog.id).to_json)
|
||||
.at_path("_links/self/href")
|
||||
|
||||
expect(last_response.body)
|
||||
.to be_json_eql(api_v3_paths.meeting(template.id).to_json)
|
||||
.at_path("_links/meeting/href")
|
||||
end
|
||||
end
|
||||
|
||||
describe "agenda item section link consistency" do
|
||||
let!(:agenda_item) do
|
||||
create(:meeting_agenda_item,
|
||||
meeting: occurrence,
|
||||
meeting_section: backlog,
|
||||
author: current_user,
|
||||
title: "Backlog item")
|
||||
end
|
||||
let(:path) { api_v3_paths.meeting_section(backlog.id) }
|
||||
|
||||
before { get path }
|
||||
|
||||
it "resolves the section referenced by the agenda item" do
|
||||
expect(last_response).to have_http_status(:ok)
|
||||
expect(last_response.body).to be_json_eql(backlog.id.to_json).at_path("id")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /api/v3/meeting_sections/:id" do
|
||||
let(:path) { api_v3_paths.meeting_section(section.id) }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user