Merge pull request #23605 from opf/feat/flat-meeting-objects

Flat meeting API objects
This commit is contained in:
Oliver Günther
2026-06-09 09:06:44 +02:00
committed by GitHub
32 changed files with 969 additions and 532 deletions
+14 -2
View File
@@ -272,20 +272,32 @@ paths:
"/api/v3/meetings/{id}":
"$ref": "./paths/meeting.yml"
"/api/v3/meetings/{id}/agenda_items":
"$ref": "./paths/meeting_agenda_items.yml"
"$ref": "./paths/meeting_agenda_items_by_meeting.yml"
"/api/v3/meetings/{meeting_id}/agenda_items/{id}":
"$ref": "./paths/meeting_agenda_item_by_meeting.yml"
"/api/v3/meeting_agenda_items":
"$ref": "./paths/meeting_agenda_items.yml"
"/api/v3/meeting_agenda_items/{id}":
"$ref": "./paths/meeting_agenda_item.yml"
"/api/v3/meetings/{meeting_id}/agenda_items/{agenda_item_id}/outcomes":
"$ref": "./paths/meeting_agenda_item_outcomes.yml"
"/api/v3/meetings/{meeting_id}/agenda_items/{agenda_item_id}/outcomes/{id}":
"$ref": "./paths/meeting_agenda_item_outcome.yml"
"/api/v3/meeting_outcomes":
"$ref": "./paths/meeting_outcomes.yml"
"/api/v3/meeting_outcomes/{id}":
"$ref": "./paths/meeting_outcome.yml"
"/api/v3/meetings/{id}/attachments":
"$ref": "./paths/meeting_attachments.yml"
"/api/v3/meetings/{id}/form":
"$ref": "./paths/meeting_form.yml"
"/api/v3/meetings/{id}/sections":
"$ref": "./paths/meeting_sections.yml"
"$ref": "./paths/meeting_sections_by_meeting.yml"
"/api/v3/meetings/{meeting_id}/sections/{id}":
"$ref": "./paths/meeting_section_by_meeting.yml"
"/api/v3/meeting_sections":
"$ref": "./paths/meeting_sections.yml"
"/api/v3/meeting_sections/{id}":
"$ref": "./paths/meeting_section.yml"
"/api/v3/meetings/form":
"$ref": "./paths/meetings_form.yml"
+5 -50
View File
@@ -1,19 +1,12 @@
# /api/v3/meetings/{meeting_id}/agenda_items/{id}
# /api/v3/meeting_agenda_items/{id}
---
get:
summary: Get a meeting agenda item
operationId: get_meeting_agenda_item
tags:
- Meetings
description: Retrieve an individual agenda item of a meeting.
description: Retrieve an individual agenda item.
parameters:
- description: Meeting identifier
example: 1
in: path
name: meeting_id
required: true
schema:
type: integer
- description: Agenda item identifier
example: 1
in: path
@@ -40,7 +33,7 @@ get:
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the agenda item or meeting does not exist or the client does not have sufficient permissions.
Returned if the agenda item does not exist or the client does not have sufficient permissions.
patch:
summary: Update a meeting agenda item
@@ -49,13 +42,6 @@ patch:
- Meetings
description: Updates the given agenda item.
parameters:
- description: Meeting identifier
example: 1
in: path
name: meeting_id
required: true
schema:
type: integer
- description: Agenda item identifier
example: 1
in: path
@@ -82,12 +68,6 @@ patch:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:MissingPermission
message: You are not authorized to access this resource.
description: |-
Returned if the client does not have sufficient permissions.
@@ -97,14 +77,8 @@ patch:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the agenda item or meeting does not exist.
Returned if the agenda item does not exist or the client does not have sufficient permissions.
'406':
$ref: "../components/responses/missing_content_type.yml"
'415':
@@ -122,13 +96,6 @@ delete:
- Meetings
description: Deletes the agenda item.
parameters:
- description: Meeting identifier
example: 1
in: path
name: meeting_id
required: true
schema:
type: integer
- description: Agenda item identifier
example: 1
in: path
@@ -144,12 +111,6 @@ delete:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:MissingPermission
message: You are not authorized to access this resource.
description: |-
Returned if the client does not have sufficient permissions.
@@ -159,11 +120,5 @@ delete:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the agenda item or meeting does not exist.
Returned if the agenda item does not exist or the client does not have sufficient permissions.
@@ -0,0 +1,43 @@
# /api/v3/meetings/{meeting_id}/agenda_items/{id}
---
get:
summary: Get a meeting agenda item
operationId: get_meeting_agenda_item_by_meeting
tags:
- Meetings
description: Retrieve an individual agenda item of a meeting.
parameters:
- description: Meeting identifier
example: 1
in: path
name: meeting_id
required: true
schema:
type: integer
- description: Agenda item identifier
example: 1
in: path
name: id
required: true
schema:
type: integer
responses:
'200':
description: OK
content:
application/hal+json:
schema:
$ref: "../components/schemas/meeting_agenda_item_model.yml"
'404':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the agenda item or meeting does not exist or the client does not have sufficient permissions.
@@ -2,7 +2,7 @@
---
get:
summary: Get a meeting outcome
operationId: get_meeting_outcome
operationId: get_meeting_outcome_by_agenda_item
tags:
- Meetings
description: Retrieve an individual outcome of a meeting agenda item.
@@ -48,143 +48,3 @@ get:
message: The requested resource could not be found.
description: |-
Returned if the outcome, agenda item, or meeting does not exist or the client does not have sufficient permissions.
patch:
summary: Update a meeting outcome
operationId: update_meeting_outcome
tags:
- Meetings
description: Updates the given meeting outcome.
parameters:
- description: Meeting identifier
example: 1
in: path
name: meeting_id
required: true
schema:
type: integer
- description: Agenda item identifier
example: 1
in: path
name: agenda_item_id
required: true
schema:
type: integer
- description: Outcome identifier
example: 1
in: path
name: id
required: true
schema:
type: integer
requestBody:
content:
application/json:
schema:
$ref: "../components/schemas/meeting_outcome_write_model.yml"
responses:
'200':
description: OK
content:
application/hal+json:
schema:
$ref: "../components/schemas/meeting_outcome_model.yml"
'400':
$ref: "../components/responses/invalid_request_body.yml"
'403':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:MissingPermission
message: You are not authorized to access this resource.
description: |-
Returned if the client does not have sufficient permissions.
**Required permission:** manage outcomes
'404':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the outcome, agenda item, or meeting does not exist.
'406':
$ref: "../components/responses/missing_content_type.yml"
'415':
$ref: "../components/responses/unsupported_media_type.yml"
'422':
description: |-
Returned if:
* a constraint for a property was violated (`PropertyConstraintViolation`)
delete:
summary: Delete a meeting outcome
operationId: delete_meeting_outcome
tags:
- Meetings
description: Deletes the outcome.
parameters:
- description: Meeting identifier
example: 1
in: path
name: meeting_id
required: true
schema:
type: integer
- description: Agenda item identifier
example: 1
in: path
name: agenda_item_id
required: true
schema:
type: integer
- description: Outcome identifier
example: 1
in: path
name: id
required: true
schema:
type: integer
responses:
'204':
description: Returned if the outcome was successfully deleted
'403':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:MissingPermission
message: You are not authorized to access this resource.
description: |-
Returned if the client does not have sufficient permissions.
**Required permission:** manage outcomes
'404':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the outcome, agenda item, or meeting does not exist.
@@ -43,76 +43,3 @@ get:
Returned if the agenda item or meeting does not exist or the client does not have sufficient permissions to see it.
**Required permission:** view meetings
post:
summary: Create meeting outcome
operationId: create_meeting_outcome
tags:
- Meetings
description: Creates a new outcome for the given meeting agenda item.
parameters:
- description: Meeting identifier
example: 1
in: path
name: meeting_id
required: true
schema:
type: integer
- description: Agenda item identifier
example: 1
in: path
name: agenda_item_id
required: true
schema:
type: integer
requestBody:
content:
application/json:
schema:
$ref: "../components/schemas/meeting_outcome_write_model.yml"
responses:
'201':
description: Created
content:
application/hal+json:
schema:
$ref: "../components/schemas/meeting_outcome_model.yml"
'400':
$ref: "../components/responses/invalid_request_body.yml"
'403':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:MissingPermission
message: You are not authorized to access this resource.
description: |-
Returned if the client does not have sufficient permissions.
**Required permission:** manage outcomes
'404':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the agenda item or meeting does not exist or the client does not have sufficient permissions to see it.
'406':
$ref: "../components/responses/missing_content_type.yml"
'415':
$ref: "../components/responses/unsupported_media_type.yml"
'422':
description: |-
Returned if:
* a constraint for a property was violated (`PropertyConstraintViolation`)
+3 -48
View File
@@ -1,56 +1,11 @@
# /api/v3/meetings/{id}/agenda_items
# /api/v3/meeting_agenda_items
---
get:
summary: List meeting agenda items
operationId: list_meeting_agenda_items
tags:
- Meetings
description: Lists all agenda items for the given meeting.
parameters:
- description: Meeting identifier
example: 1
in: path
name: id
required: true
schema:
type: integer
responses:
'200':
description: OK
content:
application/hal+json:
schema:
$ref: "../components/schemas/meeting_agenda_item_collection_model.yml"
'404':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the meeting does not exist or the client does not have sufficient permissions to see it.
**Required permission:** view meetings
post:
summary: Create meeting agenda item
operationId: create_meeting_agenda_item
tags:
- Meetings
description: Creates a new agenda item for the given meeting.
parameters:
- description: Meeting identifier
example: 1
in: path
name: id
required: true
schema:
type: integer
description: Creates a new agenda item. The request body must link the meeting.
requestBody:
content:
application/json:
@@ -92,7 +47,7 @@ post:
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the meeting does not exist or the client does not have sufficient permissions to see it.
Returned if the linked meeting does not exist or the client does not have sufficient permissions to see it.
'406':
$ref: "../components/responses/missing_content_type.yml"
'415':
@@ -0,0 +1,38 @@
# /api/v3/meetings/{id}/agenda_items
---
get:
summary: List meeting agenda items
operationId: list_meeting_agenda_items
tags:
- Meetings
description: Lists all agenda items for the given meeting.
parameters:
- description: Meeting identifier
example: 1
in: path
name: id
required: true
schema:
type: integer
responses:
'200':
description: OK
content:
application/hal+json:
schema:
$ref: "../components/schemas/meeting_agenda_item_collection_model.yml"
'404':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the meeting does not exist or the client does not have sufficient permissions to see it.
**Required permission:** view meetings
+124
View File
@@ -0,0 +1,124 @@
# /api/v3/meeting_outcomes/{id}
---
get:
summary: Get a meeting outcome
operationId: get_meeting_outcome
tags:
- Meetings
description: Retrieve an individual meeting outcome.
parameters:
- description: Outcome identifier
example: 1
in: path
name: id
required: true
schema:
type: integer
responses:
'200':
description: OK
content:
application/hal+json:
schema:
$ref: "../components/schemas/meeting_outcome_model.yml"
'404':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the outcome does not exist or the client does not have sufficient permissions.
patch:
summary: Update a meeting outcome
operationId: update_meeting_outcome
tags:
- Meetings
description: Updates the given meeting outcome.
parameters:
- description: Outcome identifier
example: 1
in: path
name: id
required: true
schema:
type: integer
requestBody:
content:
application/json:
schema:
$ref: "../components/schemas/meeting_outcome_write_model.yml"
responses:
'200':
description: OK
content:
application/hal+json:
schema:
$ref: "../components/schemas/meeting_outcome_model.yml"
'400':
$ref: "../components/responses/invalid_request_body.yml"
'403':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
description: |-
Returned if the client does not have sufficient permissions.
**Required permission:** manage outcomes
'404':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
description: |-
Returned if the outcome does not exist or the client does not have sufficient permissions.
'406':
$ref: "../components/responses/missing_content_type.yml"
'415':
$ref: "../components/responses/unsupported_media_type.yml"
'422':
description: |-
Returned if:
* a constraint for a property was violated (`PropertyConstraintViolation`)
delete:
summary: Delete a meeting outcome
operationId: delete_meeting_outcome
tags:
- Meetings
description: Deletes the outcome.
parameters:
- description: Outcome identifier
example: 1
in: path
name: id
required: true
schema:
type: integer
responses:
'204':
description: Returned if the outcome was successfully deleted
'403':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
description: |-
Returned if the client does not have sufficient permissions.
**Required permission:** manage outcomes
'404':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
description: |-
Returned if the outcome does not exist or the client does not have sufficient permissions.
+59
View File
@@ -0,0 +1,59 @@
# /api/v3/meeting_outcomes
---
post:
summary: Create meeting outcome
operationId: create_meeting_outcome
tags:
- Meetings
description: Creates a new meeting outcome. The request body must link the agenda item.
requestBody:
content:
application/json:
schema:
$ref: "../components/schemas/meeting_outcome_write_model.yml"
responses:
'201':
description: Created
content:
application/hal+json:
schema:
$ref: "../components/schemas/meeting_outcome_model.yml"
'400':
$ref: "../components/responses/invalid_request_body.yml"
'403':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:MissingPermission
message: You are not authorized to access this resource.
description: |-
Returned if the client does not have sufficient permissions.
**Required permission:** manage outcomes
'404':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the linked agenda item does not exist or the client does not have sufficient permissions to see it.
'406':
$ref: "../components/responses/missing_content_type.yml"
'415':
$ref: "../components/responses/unsupported_media_type.yml"
'422':
description: |-
Returned if:
* a constraint for a property was violated (`PropertyConstraintViolation`)
+7 -58
View File
@@ -1,19 +1,12 @@
# /api/v3/meetings/{meeting_id}/sections/{id}
# /api/v3/meeting_sections/{id}
---
get:
summary: Get a meeting section
operationId: get_meeting_section
tags:
- Meetings
description: Retrieve an individual section of a meeting.
description: Retrieve an individual meeting section.
parameters:
- description: Meeting identifier
example: 1
in: path
name: meeting_id
required: true
schema:
type: integer
- description: Section identifier
example: 1
in: path
@@ -33,29 +26,16 @@ get:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the section or meeting does not exist or the client does not have sufficient permissions.
Returned if the section does not exist or the client does not have sufficient permissions.
patch:
summary: Update a meeting section
operationId: update_meeting_section
tags:
- Meetings
description: Updates the given section.
description: Updates the given meeting section.
parameters:
- description: Meeting identifier
example: 1
in: path
name: meeting_id
required: true
schema:
type: integer
- description: Section identifier
example: 1
in: path
@@ -82,12 +62,6 @@ patch:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:MissingPermission
message: You are not authorized to access this resource.
description: |-
Returned if the client does not have sufficient permissions.
@@ -97,14 +71,8 @@ patch:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the section or meeting does not exist.
Returned if the section does not exist or the client does not have sufficient permissions.
'406':
$ref: "../components/responses/missing_content_type.yml"
'415':
@@ -120,15 +88,8 @@ delete:
operationId: delete_meeting_section
tags:
- Meetings
description: Deletes the section and all its agenda items.
description: Deletes the meeting section.
parameters:
- description: Meeting identifier
example: 1
in: path
name: meeting_id
required: true
schema:
type: integer
- description: Section identifier
example: 1
in: path
@@ -144,12 +105,6 @@ delete:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:MissingPermission
message: You are not authorized to access this resource.
description: |-
Returned if the client does not have sufficient permissions.
@@ -159,11 +114,5 @@ delete:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the section or meeting does not exist.
Returned if the section does not exist or the client does not have sufficient permissions.
@@ -0,0 +1,43 @@
# /api/v3/meetings/{meeting_id}/sections/{id}
---
get:
summary: Get a meeting section
operationId: get_meeting_section_by_meeting
tags:
- Meetings
description: Retrieve an individual section of a meeting.
parameters:
- description: Meeting identifier
example: 1
in: path
name: meeting_id
required: true
schema:
type: integer
- description: Section identifier
example: 1
in: path
name: id
required: true
schema:
type: integer
responses:
'200':
description: OK
content:
application/hal+json:
schema:
$ref: "../components/schemas/meeting_section_model.yml"
'404':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the section or meeting does not exist or the client does not have sufficient permissions.
+3 -48
View File
@@ -1,56 +1,11 @@
# /api/v3/meetings/{id}/sections
# /api/v3/meeting_sections
---
get:
summary: List meeting sections
operationId: list_meeting_sections
tags:
- Meetings
description: Lists all sections for the given meeting.
parameters:
- description: Meeting identifier
example: 1
in: path
name: id
required: true
schema:
type: integer
responses:
'200':
description: OK
content:
application/hal+json:
schema:
$ref: "../components/schemas/meeting_section_collection_model.yml"
'404':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the meeting does not exist or the client does not have sufficient permissions to see it.
**Required permission:** view meetings
post:
summary: Create meeting section
operationId: create_meeting_section
tags:
- Meetings
description: Creates a new section for the given meeting.
parameters:
- description: Meeting identifier
example: 1
in: path
name: id
required: true
schema:
type: integer
description: Creates a new section. The request body must link the meeting.
requestBody:
content:
application/json:
@@ -92,7 +47,7 @@ post:
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the meeting does not exist or the client does not have sufficient permissions to see it.
Returned if the linked meeting does not exist or the client does not have sufficient permissions to see it.
'406':
$ref: "../components/responses/missing_content_type.yml"
'415':
@@ -0,0 +1,38 @@
# /api/v3/meetings/{id}/sections
---
get:
summary: List meeting sections
operationId: list_meeting_sections
tags:
- Meetings
description: Lists all sections for the given meeting.
parameters:
- description: Meeting identifier
example: 1
in: path
name: id
required: true
schema:
type: integer
responses:
'200':
description: OK
content:
application/hal+json:
schema:
$ref: "../components/schemas/meeting_section_collection_model.yml"
'404':
content:
application/hal+json:
schema:
$ref: "../components/schemas/error_response.yml"
examples:
response:
value:
_type: Error
errorIdentifier: urn:openproject-org:api:v3:errors:NotFound
message: The requested resource could not be found.
description: |-
Returned if the meeting does not exist or the client does not have sufficient permissions to see it.
**Required permission:** view meetings
+12
View File
@@ -34,6 +34,18 @@ OPENPROJECT_SSRF_PROTECTION_IP_ALLOWLIST=2001:db8:100::/48
The list accepts one or multiple IP addresses or ranges (in CIDR notation) that shall be exempt from SSRF filtering.
### Meeting API structure changes
17.6. introduces new endpoints for meeting outcomes,
and changes the self link for all meeting related resources to be flat:
That means, some of the responses have changed:
POST/PATCH/DELETE `/api/v3/meetings/:id/agenda_items)` is no longer available,
they have been moved to the `/api/v3/meeting_agendas/` respectively. The same is true for outcomes and sections.
This follows the APIv3 standards, and also fixes a bug related to the self link.
<!-- BEGIN SECURITY FIXES AUTOMATED SECTION -->
<!-- END SECURITY FIXES AUTOMATED SECTION -->
@@ -36,17 +36,10 @@ module API
get do
items = @meeting.agenda_items.includes(:author, :presenter, :work_package, :meeting_section)
MeetingAgendaItemCollectionRepresenter.new(items,
self_link: api_v3_paths.meeting_agenda_items(@meeting.id),
self_link: api_v3_paths.meeting_agenda_items(meeting_id: @meeting.id),
current_user:)
end
post(&::API::V3::Utilities::Endpoints::Create
.new(model: MeetingAgendaItem,
params_modifier: ->(params) {
params.except(:meeting, :meeting_id).merge(meeting: @meeting)
})
.mount)
route_param :agenda_item_id, type: Integer, desc: "Agenda item ID" do
after_validation do
@meeting_agenda_item = @meeting.agenda_items.find(declared_params[:agenda_item_id])
@@ -54,10 +47,6 @@ module API
get &::API::V3::Utilities::Endpoints::Show.new(model: MeetingAgendaItem).mount
patch &::API::V3::Utilities::Endpoints::Update.new(model: MeetingAgendaItem).mount
delete &::API::V3::Utilities::Endpoints::Delete.new(model: MeetingAgendaItem).mount
mount ::API::V3::MeetingOutcomes::OutcomesByAgendaItemAPI
end
end
@@ -42,8 +42,7 @@ module API
{ outcomes: %i[author work_package] }
]
self_link id_attribute: ->(*) { [represented.meeting_id, represented.id] },
title_getter: ->(*) { represented.title }
self_link title_getter: ->(*) { represented.title }
property :id
@@ -88,7 +87,7 @@ module API
next if represented.meeting_section_id.nil?
{
href: api_v3_paths.meeting_section(represented.meeting_id, represented.meeting_section_id),
href: api_v3_paths.meeting_section(represented.meeting_section_id),
title: represented.meeting_section&.title
}
}
@@ -102,8 +101,7 @@ module API
link: ->(*) {
represented.outcomes.map do |outcome|
{
href: api_v3_paths
.meeting_agenda_item_outcome(represented.meeting_id, represented.id, outcome.id),
href: api_v3_paths.meeting_outcome(outcome.id),
title: outcome.id.to_s
}
end
@@ -0,0 +1,56 @@
# 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 MeetingAgendaItems
class MeetingAgendaItemsAPI < ::API::OpenProjectAPI
resources :meeting_agenda_items do
post &::API::V3::Utilities::Endpoints::Create.new(model: MeetingAgendaItem).mount
route_param :id, type: Integer, desc: "Agenda item ID" do
after_validation do
@meeting_agenda_item = MeetingAgendaItem
.joins(meeting: :project)
.merge(Meeting.visible)
.find(declared_params[:id])
end
get &::API::V3::Utilities::Endpoints::Show.new(model: MeetingAgendaItem).mount
patch &::API::V3::Utilities::Endpoints::Update.new(model: MeetingAgendaItem).mount
delete &::API::V3::Utilities::Endpoints::Delete.new(model: MeetingAgendaItem).mount
end
end
end
end
end
end
@@ -39,10 +39,7 @@ module API
self.to_eager_load = [{ meeting_agenda_item: :meeting }, :author, :work_package]
self_link path: :meeting_agenda_item_outcome,
id_attribute: ->(*) {
[represented.meeting_agenda_item.meeting_id, represented.meeting_agenda_item_id, represented.id]
},
self_link path: :meeting_outcome,
title_getter: ->(*) { represented.id.to_s }
property :id
@@ -56,13 +53,16 @@ module API
representer: ::API::V3::Users::UserRepresenter,
skip_render: ->(*) { represented.author_id.nil? }
link :agendaItem do
{
href: api_v3_paths.meeting_agenda_item(represented.meeting_agenda_item.meeting_id,
represented.meeting_agenda_item_id),
title: represented.meeting_agenda_item.title
}
end
associated_resource :meeting_agenda_item,
as: :agendaItem,
link: ->(*) {
next if represented.meeting_agenda_item_id.nil?
{
href: api_v3_paths.meeting_agenda_item(represented.meeting_agenda_item_id),
title: represented.meeting_agenda_item.title
}
}
associated_visible_resource :work_package
@@ -0,0 +1,56 @@
# 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 MeetingOutcomes
class MeetingOutcomesAPI < ::API::OpenProjectAPI
resources :meeting_outcomes do
post &::API::V3::Utilities::Endpoints::Create.new(model: MeetingOutcome).mount
route_param :id, type: Integer, desc: "Outcome ID" do
after_validation do
@meeting_outcome = MeetingOutcome
.joins(meeting_agenda_item: { meeting: :project })
.merge(Meeting.visible)
.find(declared_params[:id])
end
get &::API::V3::Utilities::Endpoints::Show.new(model: MeetingOutcome).mount
patch &::API::V3::Utilities::Endpoints::Update.new(model: MeetingOutcome).mount
delete &::API::V3::Utilities::Endpoints::Delete.new(model: MeetingOutcome).mount
end
end
end
end
end
end
@@ -38,29 +38,17 @@ module API
MeetingOutcomeCollectionRepresenter.new(outcomes,
self_link: api_v3_paths
.meeting_agenda_item_outcomes(@meeting.id, @meeting_agenda_item.id),
.meeting_agenda_item_outcomes(@meeting_agenda_item.id,
meeting_id: @meeting.id),
current_user:)
end
post(&::API::V3::Utilities::Endpoints::Create
.new(model: MeetingOutcome,
params_modifier: ->(params) {
params
.except(:meeting_agenda_item, :meeting_agenda_item_id)
.merge(meeting_agenda_item: @meeting_agenda_item)
})
.mount)
route_param :outcome_id, type: Integer, desc: "Outcome ID" do
after_validation do
@meeting_outcome = @meeting_agenda_item.outcomes.find(declared_params[:outcome_id])
end
get &::API::V3::Utilities::Endpoints::Show.new(model: MeetingOutcome).mount
patch &::API::V3::Utilities::Endpoints::Update.new(model: MeetingOutcome).mount
delete &::API::V3::Utilities::Endpoints::Delete.new(model: MeetingOutcome).mount
end
end
end
@@ -38,8 +38,7 @@ module API
self.to_eager_load = [:meeting]
self_link id_attribute: ->(*) { [represented.meeting_id, represented.id] },
title_getter: ->(*) { represented.title }
self_link title_getter: ->(*) { represented.title }
property :id
@@ -0,0 +1,56 @@
# 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 MeetingSections
class MeetingSectionsAPI < ::API::OpenProjectAPI
resources :meeting_sections do
post &::API::V3::Utilities::Endpoints::Create.new(model: MeetingSection).mount
route_param :id, type: Integer, desc: "Section ID" do
after_validation do
@meeting_section = MeetingSection
.joins(meeting: :project)
.merge(Meeting.visible)
.find(declared_params[:id])
end
get &::API::V3::Utilities::Endpoints::Show.new(model: MeetingSection).mount
patch &::API::V3::Utilities::Endpoints::Update.new(model: MeetingSection).mount
delete &::API::V3::Utilities::Endpoints::Delete.new(model: MeetingSection).mount
end
end
end
end
end
end
@@ -36,27 +36,16 @@ module API
get do
sections = @meeting.sections
MeetingSectionCollectionRepresenter.new(sections,
self_link: api_v3_paths.meeting_sections(@meeting.id),
self_link: api_v3_paths.meeting_sections(meeting_id: @meeting.id),
current_user:)
end
post(&::API::V3::Utilities::Endpoints::Create
.new(model: MeetingSection,
params_modifier: ->(params) {
params.except(:meeting, :meeting_id).merge(meeting: @meeting)
})
.mount)
route_param :section_id, type: Integer, desc: "Section ID" do
after_validation do
@meeting_section = @meeting.sections.find(declared_params[:section_id])
end
get &::API::V3::Utilities::Endpoints::Show.new(model: MeetingSection).mount
patch &::API::V3::Utilities::Endpoints::Update.new(model: MeetingSection).mount
delete &::API::V3::Utilities::Endpoints::Delete.new(model: MeetingSection).mount
end
end
end
@@ -76,13 +76,13 @@ module API
link :agendaItems do
{
href: api_v3_paths.meeting_agenda_items(represented.id)
href: api_v3_paths.meeting_agenda_items(meeting_id: represented.id)
}
end
link :sections do
{
href: api_v3_paths.meeting_sections(represented.id)
href: api_v3_paths.meeting_sections(meeting_id: represented.id)
}
end
@@ -1,4 +1,5 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -58,6 +59,10 @@ module API
mount ::API::V3::MeetingSections::SectionsByMeetingAPI
end
end
mount ::API::V3::MeetingAgendaItems::MeetingAgendaItemsAPI
mount ::API::V3::MeetingSections::MeetingSectionsAPI
mount ::API::V3::MeetingOutcomes::MeetingOutcomesAPI
end
end
end
@@ -241,28 +241,52 @@ module OpenProject::Meeting
"#{root}/meetings/#{id}/form"
end
add_api_path :meeting_agenda_items do |meeting_id|
"#{meeting(meeting_id)}/agenda_items"
add_api_path :meeting_agenda_items do |meeting_id: nil|
if meeting_id
"#{meeting(meeting_id)}/agenda_items"
else
"#{root}/meeting_agenda_items"
end
end
add_api_path :meeting_agenda_item do |meeting_id, id|
"#{meeting(meeting_id)}/agenda_items/#{id}"
add_api_path :meeting_agenda_item do |id, meeting_id: nil|
if meeting_id
"#{meeting(meeting_id)}/agenda_items/#{id}"
else
"#{root}/meeting_agenda_items/#{id}"
end
end
add_api_path :meeting_agenda_item_outcomes do |meeting_id, agenda_item_id|
"#{meeting_agenda_item(meeting_id, agenda_item_id)}/outcomes"
add_api_path :meeting_agenda_item_outcomes do |agenda_item_id, meeting_id: nil|
"#{meeting_agenda_item(agenda_item_id, meeting_id:)}/outcomes"
end
add_api_path :meeting_agenda_item_outcome do |meeting_id, agenda_item_id, id|
"#{meeting_agenda_item_outcomes(meeting_id, agenda_item_id)}/#{id}"
add_api_path :meeting_outcomes do
"#{root}/meeting_outcomes"
end
add_api_path :meeting_sections do |meeting_id|
"#{meeting(meeting_id)}/sections"
add_api_path :meeting_outcome do |id|
"#{root}/meeting_outcomes/#{id}"
end
add_api_path :meeting_section do |meeting_id, id|
"#{meeting(meeting_id)}/sections/#{id}"
add_api_path :meeting_agenda_item_outcome do |id, agenda_item_id:, meeting_id: nil|
"#{meeting_agenda_item_outcomes(agenda_item_id, meeting_id:)}/#{id}"
end
add_api_path :meeting_sections do |meeting_id: nil|
if meeting_id
"#{meeting(meeting_id)}/sections"
else
"#{root}/meeting_sections"
end
end
add_api_path :meeting_section do |id, meeting_id: nil|
if meeting_id
"#{meeting(meeting_id)}/sections/#{id}"
else
"#{root}/meeting_sections/#{id}"
end
end
add_api_path :recurring_meetings do
@@ -35,6 +35,7 @@ RSpec.describe MeetingSections::CreateContract do
include_context "ModelContract shared context"
shared_let(:project) { create(:project) }
shared_let(:other_project) { create(:project) }
let(:meeting) { create(:meeting, project:) }
let(:section) { build(:meeting_section, meeting:) }
let(:contract) { described_class.new(section, user) }
@@ -79,6 +80,14 @@ RSpec.describe MeetingSections::CreateContract do
it_behaves_like "contract is invalid", base: :does_not_exist
end
context "with permission in another project" do
let(:user) do
create(:user, member_with_permissions: { other_project => %i[view_meetings manage_agendas] })
end
it_behaves_like "contract is invalid", base: :does_not_exist
end
include_examples "contract reuses the model errors" do
let(:user) { build_stubbed(:user) }
end
@@ -35,6 +35,7 @@ RSpec.describe MeetingSections::DeleteContract do
include_context "ModelContract shared context"
shared_let(:project) { create(:project) }
shared_let(:other_project) { create(:project) }
shared_let(:meeting) { create(:meeting, project:) }
let(:section) { create(:meeting_section, meeting:) }
let(:contract) { described_class.new(section, user) }
@@ -69,6 +70,14 @@ RSpec.describe MeetingSections::DeleteContract do
it_behaves_like "contract is invalid", base: :error_unauthorized
end
context "with permission in another project" do
let(:user) do
create(:user, member_with_permissions: { other_project => %i[view_meetings manage_agendas] })
end
it_behaves_like "contract is invalid", base: :error_unauthorized
end
include_examples "contract reuses the model errors" do
let(:user) { build_stubbed(:user) }
end
@@ -35,6 +35,7 @@ RSpec.describe MeetingSections::UpdateContract do
include_context "ModelContract shared context"
shared_let(:project) { create(:project) }
shared_let(:other_project) { create(:project) }
shared_let(:meeting) { create(:meeting, project:) }
shared_let(:section) { create(:meeting_section, meeting:) }
let(:contract) { described_class.new(section, user) }
@@ -61,6 +62,14 @@ RSpec.describe MeetingSections::UpdateContract do
it_behaves_like "contract is invalid", base: :error_unauthorized
end
context "with permission in another project" do
let(:user) do
create(:user, member_with_permissions: { other_project => %i[view_meetings manage_agendas] })
end
it_behaves_like "contract is invalid", base: :error_unauthorized
end
include_examples "contract reuses the model errors" do
let(:user) { build_stubbed(:user) }
end
@@ -51,7 +51,7 @@ RSpec.describe "API v3 Meeting Agenda Items sub-resource", content_type: :json d
end
describe "GET /api/v3/meetings/:meeting_id/agenda_items" do
let(:path) { api_v3_paths.meeting_agenda_items(meeting.id) }
let(:path) { api_v3_paths.meeting_agenda_items(meeting_id: meeting.id) }
before { get path }
@@ -69,6 +69,18 @@ RSpec.describe "API v3 Meeting Agenda Items sub-resource", content_type: :json d
expect(last_response.body)
.to have_json_size(1)
.at_path("_embedded/elements/0/_embedded/outcomes")
expect(last_response.body)
.to be_json_eql(api_v3_paths.meeting_outcome(outcome.id).to_json)
.at_path("_embedded/elements/0/_links/outcomes/0/href")
expect(last_response.body)
.to be_json_eql(api_v3_paths.meeting_agenda_item(agenda_item.id).to_json)
.at_path("_embedded/elements/0/_links/self/href")
expect(last_response.body)
.to be_json_eql(api_v3_paths.meeting_section(section.id).to_json)
.at_path("_embedded/elements/0/_links/section/href")
end
context "without view_meetings permission" do
@@ -80,11 +92,16 @@ RSpec.describe "API v3 Meeting Agenda Items sub-resource", content_type: :json d
end
end
describe "POST /api/v3/meetings/:meeting_id/agenda_items" do
let(:path) { api_v3_paths.meeting_agenda_items(meeting.id) }
describe "POST /api/v3/meeting_agenda_items" do
let(:path) { api_v3_paths.meeting_agenda_items }
let(:body) do
{
title: "New agenda item"
title: "New agenda item",
_links: {
meeting: {
href: api_v3_paths.meeting(meeting.id)
}
}
}.to_json
end
@@ -109,6 +126,44 @@ RSpec.describe "API v3 Meeting Agenda Items sub-resource", content_type: :json d
.at_path("title")
end
context "with a section href returned by the section collection" do
let!(:target_section) { create(:meeting_section, meeting:) }
let(:target_section_href) do
get api_v3_paths.meeting_sections(meeting_id: meeting.id)
JSON
.parse(last_response.body)
.dig("_embedded", "elements")
.find { |item| item["id"] == target_section.id }
.dig("_links", "self", "href")
end
let(:body) do
{
title: "New agenda item in target section",
_links: {
meeting: {
href: api_v3_paths.meeting(meeting.id)
},
section: {
href: target_section_href
}
}
}.to_json
end
it "responds with 201" do
expect(target_section_href).to eq(api_v3_paths.meeting_section(target_section.id))
expect(response).to have_http_status(:created)
end
it "creates the agenda item in that section" do
response
expect(meeting.agenda_items.find_by(title: "New agenda item in target section").meeting_section)
.to eq(target_section)
end
end
context "without manage_agendas permission" do
let(:permissions) { %i[view_meetings] }
@@ -119,7 +174,7 @@ RSpec.describe "API v3 Meeting Agenda Items sub-resource", content_type: :json d
end
describe "GET /api/v3/meetings/:meeting_id/agenda_items/:id" do
let(:path) { api_v3_paths.meeting_agenda_item(meeting.id, agenda_item.id) }
let(:path) { api_v3_paths.meeting_agenda_item(agenda_item.id, meeting_id: meeting.id) }
before { get path }
@@ -141,7 +196,7 @@ RSpec.describe "API v3 Meeting Agenda Items sub-resource", content_type: :json d
context "with an item from another meeting" do
let(:other_meeting) { create(:meeting, project:, author: current_user) }
let(:path) { api_v3_paths.meeting_agenda_item(other_meeting.id, agenda_item.id) }
let(:path) { api_v3_paths.meeting_agenda_item(agenda_item.id, meeting_id: other_meeting.id) }
it "returns 404" do
expect(last_response).to have_http_status(:not_found)
@@ -155,7 +210,7 @@ RSpec.describe "API v3 Meeting Agenda Items sub-resource", content_type: :json d
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) }
let(:path) { api_v3_paths.meeting_agenda_item(wp_agenda_item.id, meeting_id: meeting.id) }
it "returns 200" do
expect(last_response).to have_http_status(:ok)
@@ -173,8 +228,34 @@ RSpec.describe "API v3 Meeting Agenda Items sub-resource", content_type: :json d
end
end
describe "PATCH /api/v3/meetings/:meeting_id/agenda_items/:id" do
let(:path) { api_v3_paths.meeting_agenda_item(meeting.id, agenda_item.id) }
describe "GET /api/v3/meeting_agenda_items/:id" do
let(:path) { api_v3_paths.meeting_agenda_item(agenda_item.id) }
before { get path }
it "returns 200 and the agenda item" do
expect(last_response).to have_http_status(:ok)
expect(last_response.body)
.to be_json_eql("MeetingAgendaItem".to_json)
.at_path("_type")
expect(last_response.body)
.to be_json_eql(api_v3_paths.meeting_agenda_item(agenda_item.id).to_json)
.at_path("_links/self/href")
end
context "without view_meetings permission" do
let(:permissions) { [] }
it "returns 404" do
expect(last_response).to have_http_status(:not_found)
end
end
end
describe "PATCH /api/v3/meeting_agenda_items/:id" do
let(:path) { api_v3_paths.meeting_agenda_item(agenda_item.id) }
let(:body) do
{
title: "Updated title",
@@ -193,6 +274,42 @@ RSpec.describe "API v3 Meeting Agenda Items sub-resource", content_type: :json d
expect(agenda_item.reload.title).to eq("Updated title")
end
context "with a section href returned by the agenda item collection" do
let(:target_section) { create(:meeting_section, meeting:) }
let!(:target_agenda_item) do
create(:meeting_agenda_item, meeting:, meeting_section: target_section, author: current_user)
end
let(:target_section_href) do
get api_v3_paths.meeting_agenda_items(meeting_id: meeting.id)
JSON
.parse(last_response.body)
.dig("_embedded", "elements")
.find { |item| item["id"] == target_agenda_item.id }
.dig("_links", "section", "href")
end
let(:body) do
{
lockVersion: agenda_item.lock_version,
_links: {
section: {
href: target_section_href
}
}
}.to_json
end
it "responds with 200" do
expect(target_section_href).to eq(api_v3_paths.meeting_section(target_section.id))
expect(response).to have_http_status(:ok)
end
it "moves the agenda item to that section" do
response
expect(agenda_item.reload.meeting_section).to eq(target_section)
end
end
context "without manage_agendas permission" do
let(:permissions) { %i[view_meetings] }
@@ -202,8 +319,8 @@ RSpec.describe "API v3 Meeting Agenda Items sub-resource", content_type: :json d
end
end
describe "DELETE /api/v3/meetings/:meeting_id/agenda_items/:id" do
let(:path) { api_v3_paths.meeting_agenda_item(meeting.id, agenda_item.id) }
describe "DELETE /api/v3/meeting_agenda_items/:id" do
let(:path) { api_v3_paths.meeting_agenda_item(agenda_item.id) }
before { delete path }
@@ -51,7 +51,7 @@ RSpec.describe "API v3 Meeting Outcomes sub-resource", content_type: :json do
end
describe "GET /api/v3/meetings/:meeting_id/agenda_items/:agenda_item_id/outcomes" do
let(:path) { api_v3_paths.meeting_agenda_item_outcomes(meeting.id, agenda_item.id) }
let(:path) { api_v3_paths.meeting_agenda_item_outcomes(agenda_item.id, meeting_id: meeting.id) }
before { get path }
@@ -65,11 +65,19 @@ RSpec.describe "API v3 Meeting Outcomes sub-resource", content_type: :json do
expect(last_response.body)
.to have_json_size(1)
.at_path("_embedded/elements")
expect(last_response.body)
.to be_json_eql(api_v3_paths.meeting_outcome(outcome.id).to_json)
.at_path("_embedded/elements/0/_links/self/href")
expect(last_response.body)
.to be_json_eql(api_v3_paths.meeting_agenda_item(agenda_item.id).to_json)
.at_path("_embedded/elements/0/_links/agendaItem/href")
end
context "with an agenda item from another meeting" do
let(:other_meeting) { create(:meeting, project:, author: current_user) }
let(:path) { api_v3_paths.meeting_agenda_item_outcomes(other_meeting.id, agenda_item.id) }
let(:path) { api_v3_paths.meeting_agenda_item_outcomes(agenda_item.id, meeting_id: other_meeting.id) }
it "returns 404" do
expect(last_response).to have_http_status(:not_found)
@@ -108,12 +116,17 @@ RSpec.describe "API v3 Meeting Outcomes sub-resource", content_type: :json do
end
end
describe "POST /api/v3/meetings/:meeting_id/agenda_items/:agenda_item_id/outcomes" do
let(:path) { api_v3_paths.meeting_agenda_item_outcomes(meeting.id, agenda_item.id) }
describe "POST /api/v3/meeting_outcomes" do
let(:path) { api_v3_paths.meeting_outcomes }
let(:body) do
{
kind: "information",
notes: { raw: "Outcome created via API" }
notes: { raw: "Outcome created via API" },
_links: {
agendaItem: {
href: api_v3_paths.meeting_agenda_item(agenda_item.id)
}
}
}.to_json
end
@@ -153,6 +166,9 @@ RSpec.describe "API v3 Meeting Outcomes sub-resource", content_type: :json do
{
kind: "work_package",
_links: {
agendaItem: {
href: api_v3_paths.meeting_agenda_item(agenda_item.id)
},
workPackage: {
href: api_v3_paths.work_package(work_package.id)
}
@@ -182,6 +198,9 @@ RSpec.describe "API v3 Meeting Outcomes sub-resource", content_type: :json do
{
kind: "work_package",
_links: {
agendaItem: {
href: api_v3_paths.meeting_agenda_item(agenda_item.id)
},
workPackage: {
href: api_v3_paths.work_package(private_work_package.id)
}
@@ -197,7 +216,11 @@ RSpec.describe "API v3 Meeting Outcomes sub-resource", content_type: :json do
end
describe "GET /api/v3/meetings/:meeting_id/agenda_items/:agenda_item_id/outcomes/:id" do
let(:path) { api_v3_paths.meeting_agenda_item_outcome(meeting.id, agenda_item.id, outcome.id) }
let(:path) do
api_v3_paths.meeting_agenda_item_outcome(outcome.id,
agenda_item_id: agenda_item.id,
meeting_id: meeting.id)
end
before { get path }
@@ -211,11 +234,19 @@ RSpec.describe "API v3 Meeting Outcomes sub-resource", content_type: :json do
expect(last_response.body)
.to be_json_eql(outcome.id.to_json)
.at_path("id")
expect(last_response.body)
.to be_json_eql(api_v3_paths.meeting_outcome(outcome.id).to_json)
.at_path("_links/self/href")
end
context "with an outcome from another agenda item" do
let(:other_agenda_item) { create(:meeting_agenda_item, meeting:, meeting_section: section, author: current_user) }
let(:path) { api_v3_paths.meeting_agenda_item_outcome(meeting.id, other_agenda_item.id, outcome.id) }
let(:path) do
api_v3_paths.meeting_agenda_item_outcome(outcome.id,
agenda_item_id: other_agenda_item.id,
meeting_id: meeting.id)
end
it "returns 404" do
expect(last_response).to have_http_status(:not_found)
@@ -242,13 +273,38 @@ RSpec.describe "API v3 Meeting Outcomes sub-resource", content_type: :json do
.at_path("_links/workPackage/href")
expect(last_response.body).not_to have_json_path("_embedded/workPackage")
end
end
end
describe "PATCH /api/v3/meetings/:meeting_id/agenda_items/:agenda_item_id/outcomes/:id" do
let(:path) { api_v3_paths.meeting_agenda_item_outcome(meeting.id, agenda_item.id, outcome.id) }
describe "GET /api/v3/meeting_outcomes/:id" do
let(:path) { api_v3_paths.meeting_outcome(outcome.id) }
before { get path }
it "returns 200 and the outcome" do
expect(last_response).to have_http_status(:ok)
expect(last_response.body)
.to be_json_eql("MeetingOutcome".to_json)
.at_path("_type")
expect(last_response.body)
.to be_json_eql(api_v3_paths.meeting_outcome(outcome.id).to_json)
.at_path("_links/self/href")
end
context "without view_meetings permission" do
let(:permissions) { [] }
it "returns 404" do
expect(last_response).to have_http_status(:not_found)
end
end
end
describe "PATCH /api/v3/meeting_outcomes/:id" do
let(:path) { api_v3_paths.meeting_outcome(outcome.id) }
let(:body) do
{
notes: { raw: "Updated outcome" }
@@ -271,8 +327,8 @@ RSpec.describe "API v3 Meeting Outcomes sub-resource", content_type: :json do
end
end
describe "DELETE /api/v3/meetings/:meeting_id/agenda_items/:agenda_item_id/outcomes/:id" do
let(:path) { api_v3_paths.meeting_agenda_item_outcome(meeting.id, agenda_item.id, outcome.id) }
describe "DELETE /api/v3/meeting_outcomes/:id" do
let(:path) { api_v3_paths.meeting_outcome(outcome.id) }
before { delete path }
@@ -36,20 +36,42 @@ RSpec.describe "API v3 Meeting Sections sub-resource", content_type: :json do
include API::V3::Utilities::PathHelper
shared_let(:project) { create(:project, enabled_module_names: %w[meetings]) }
shared_let(:other_project) { create(:project, enabled_module_names: %w[meetings]) }
shared_let(:author) { create(:user) }
let(:permissions) { %i[view_meetings manage_agendas] }
let(:current_user) do
create(:user, member_with_permissions: { project => permissions })
end
let(:meeting) { create(:meeting, project:, author: current_user) }
let(:meeting) { create(:meeting, project:, author:) }
let!(:section) { create(:meeting_section, meeting:, title: "First Section") }
before do
login_as current_user
end
shared_examples "not found without meeting visibility" do
context "without view_meetings permission" do
let(:permissions) { [] }
it "returns 404" do
expect(last_response).to have_http_status(:not_found)
end
end
context "with view_meetings permission in another project" do
let(:current_user) do
create(:user, member_with_permissions: { other_project => %i[view_meetings manage_agendas] })
end
it "returns 404" do
expect(last_response).to have_http_status(:not_found)
end
end
end
describe "GET /api/v3/meetings/:meeting_id/sections" do
let(:path) { api_v3_paths.meeting_sections(meeting.id) }
let(:path) { api_v3_paths.meeting_sections(meeting_id: meeting.id) }
before { get path }
@@ -59,22 +81,25 @@ RSpec.describe "API v3 Meeting Sections sub-resource", content_type: :json do
expect(last_response.body)
.to be_json_eql("Collection".to_json)
.at_path("_type")
expect(last_response.body)
.to be_json_eql(api_v3_paths.meeting_section(section.id).to_json)
.at_path("_embedded/elements/0/_links/self/href")
end
context "without view_meetings permission" do
let(:permissions) { [] }
it "returns 404" do
expect(last_response).to have_http_status(:not_found)
end
end
it_behaves_like "not found without meeting visibility"
end
describe "POST /api/v3/meetings/:meeting_id/sections" do
let(:path) { api_v3_paths.meeting_sections(meeting.id) }
describe "POST /api/v3/meeting_sections" do
let(:path) { api_v3_paths.meeting_sections }
let(:body) do
{
title: "New Section"
title: "New Section",
_links: {
meeting: {
href: api_v3_paths.meeting(meeting.id)
}
}
}.to_json
end
@@ -106,10 +131,30 @@ RSpec.describe "API v3 Meeting Sections sub-resource", content_type: :json do
expect(response).to have_http_status(:forbidden)
end
end
context "without any permissions" do
let(:permissions) { [] }
it "returns 422 and does not create a section" do
expect(response).to have_http_status(:unprocessable_entity)
expect(meeting.sections.find_by(title: "New Section")).to be_nil
end
end
context "with manage_agendas permission in another project" do
let(:current_user) do
create(:user, member_with_permissions: { other_project => %i[view_meetings manage_agendas] })
end
it "returns 422 and does not create a section" do
expect(response).to have_http_status(:unprocessable_entity)
expect(meeting.sections.find_by(title: "New Section")).to be_nil
end
end
end
describe "GET /api/v3/meetings/:meeting_id/sections/:id" do
let(:path) { api_v3_paths.meeting_section(meeting.id, section.id) }
let(:path) { api_v3_paths.meeting_section(section.id, meeting_id: meeting.id) }
before { get path }
@@ -126,17 +171,39 @@ RSpec.describe "API v3 Meeting Sections sub-resource", content_type: :json do
end
context "with a section from another meeting" do
let(:other_meeting) { create(:meeting, project:, author: current_user) }
let(:path) { api_v3_paths.meeting_section(other_meeting.id, section.id) }
let(:other_meeting) { create(:meeting, project:, author:) }
let(:path) { api_v3_paths.meeting_section(section.id, meeting_id: other_meeting.id) }
it "returns 404" do
expect(last_response).to have_http_status(:not_found)
end
end
it_behaves_like "not found without meeting visibility"
end
describe "PATCH /api/v3/meetings/:meeting_id/sections/:id" do
let(:path) { api_v3_paths.meeting_section(meeting.id, section.id) }
describe "GET /api/v3/meeting_sections/:id" do
let(:path) { api_v3_paths.meeting_section(section.id) }
before { get path }
it "returns 200 and the section" do
expect(last_response).to have_http_status(:ok)
expect(last_response.body)
.to be_json_eql("MeetingSection".to_json)
.at_path("_type")
expect(last_response.body)
.to be_json_eql(api_v3_paths.meeting_section(section.id).to_json)
.at_path("_links/self/href")
end
it_behaves_like "not found without meeting visibility"
end
describe "PATCH /api/v3/meeting_sections/:id" do
let(:path) { api_v3_paths.meeting_section(section.id) }
let(:body) do
{
title: "Updated Section Title"
@@ -161,10 +228,30 @@ RSpec.describe "API v3 Meeting Sections sub-resource", content_type: :json do
expect(response).to have_http_status(:forbidden)
end
end
context "without any permissions" do
let(:permissions) { [] }
it "returns 404 and does not update the section" do
expect(response).to have_http_status(:not_found)
expect(section.reload.title).to eq("First Section")
end
end
context "with manage_agendas permission in another project" do
let(:current_user) do
create(:user, member_with_permissions: { other_project => %i[view_meetings manage_agendas] })
end
it "returns 404 and does not update the section" do
expect(response).to have_http_status(:not_found)
expect(section.reload.title).to eq("First Section")
end
end
end
describe "DELETE /api/v3/meetings/:meeting_id/sections/:id" do
let(:path) { api_v3_paths.meeting_section(meeting.id, section.id) }
describe "DELETE /api/v3/meeting_sections/:id" do
let(:path) { api_v3_paths.meeting_section(section.id) }
before { delete path }
@@ -185,5 +272,25 @@ RSpec.describe "API v3 Meeting Sections sub-resource", content_type: :json do
it_behaves_like "unauthorized access"
end
context "without any permissions" do
let(:permissions) { [] }
it "returns 404 and does not delete the section" do
expect(subject).to have_http_status(:not_found)
expect(MeetingSection).to exist(section.id)
end
end
context "with manage_agendas permission in another project" do
let(:current_user) do
create(:user, member_with_permissions: { other_project => %i[view_meetings manage_agendas] })
end
it "returns 404 and does not delete the section" do
expect(subject).to have_http_status(:not_found)
expect(MeetingSection).to exist(section.id)
end
end
end
end