diff --git a/docs/api/apiv3/openapi-spec.yml b/docs/api/apiv3/openapi-spec.yml index c47cf84d2ad..a23b2192a10 100644 --- a/docs/api/apiv3/openapi-spec.yml +++ b/docs/api/apiv3/openapi-spec.yml @@ -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" diff --git a/docs/api/apiv3/paths/meeting_agenda_item.yml b/docs/api/apiv3/paths/meeting_agenda_item.yml index 4c11743260b..8f2de6626ed 100644 --- a/docs/api/apiv3/paths/meeting_agenda_item.yml +++ b/docs/api/apiv3/paths/meeting_agenda_item.yml @@ -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. diff --git a/docs/api/apiv3/paths/meeting_agenda_item_by_meeting.yml b/docs/api/apiv3/paths/meeting_agenda_item_by_meeting.yml new file mode 100644 index 00000000000..0067285a39e --- /dev/null +++ b/docs/api/apiv3/paths/meeting_agenda_item_by_meeting.yml @@ -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. diff --git a/docs/api/apiv3/paths/meeting_agenda_item_outcome.yml b/docs/api/apiv3/paths/meeting_agenda_item_outcome.yml index 00a6172cd06..c4622ca48b6 100644 --- a/docs/api/apiv3/paths/meeting_agenda_item_outcome.yml +++ b/docs/api/apiv3/paths/meeting_agenda_item_outcome.yml @@ -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. diff --git a/docs/api/apiv3/paths/meeting_agenda_item_outcomes.yml b/docs/api/apiv3/paths/meeting_agenda_item_outcomes.yml index 13f5b8f66be..32d096816b8 100644 --- a/docs/api/apiv3/paths/meeting_agenda_item_outcomes.yml +++ b/docs/api/apiv3/paths/meeting_agenda_item_outcomes.yml @@ -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`) diff --git a/docs/api/apiv3/paths/meeting_agenda_items.yml b/docs/api/apiv3/paths/meeting_agenda_items.yml index 590180f157d..23bf5e6f549 100644 --- a/docs/api/apiv3/paths/meeting_agenda_items.yml +++ b/docs/api/apiv3/paths/meeting_agenda_items.yml @@ -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': diff --git a/docs/api/apiv3/paths/meeting_agenda_items_by_meeting.yml b/docs/api/apiv3/paths/meeting_agenda_items_by_meeting.yml new file mode 100644 index 00000000000..c49c9e7eb03 --- /dev/null +++ b/docs/api/apiv3/paths/meeting_agenda_items_by_meeting.yml @@ -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 diff --git a/docs/api/apiv3/paths/meeting_outcome.yml b/docs/api/apiv3/paths/meeting_outcome.yml new file mode 100644 index 00000000000..28581ff2423 --- /dev/null +++ b/docs/api/apiv3/paths/meeting_outcome.yml @@ -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. diff --git a/docs/api/apiv3/paths/meeting_outcomes.yml b/docs/api/apiv3/paths/meeting_outcomes.yml new file mode 100644 index 00000000000..4cf6088a753 --- /dev/null +++ b/docs/api/apiv3/paths/meeting_outcomes.yml @@ -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`) diff --git a/docs/api/apiv3/paths/meeting_section.yml b/docs/api/apiv3/paths/meeting_section.yml index 79c167db431..d06fc3dcb5c 100644 --- a/docs/api/apiv3/paths/meeting_section.yml +++ b/docs/api/apiv3/paths/meeting_section.yml @@ -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. diff --git a/docs/api/apiv3/paths/meeting_section_by_meeting.yml b/docs/api/apiv3/paths/meeting_section_by_meeting.yml new file mode 100644 index 00000000000..6eaed2fc867 --- /dev/null +++ b/docs/api/apiv3/paths/meeting_section_by_meeting.yml @@ -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. diff --git a/docs/api/apiv3/paths/meeting_sections.yml b/docs/api/apiv3/paths/meeting_sections.yml index 5d5bf04218a..db095f5071f 100644 --- a/docs/api/apiv3/paths/meeting_sections.yml +++ b/docs/api/apiv3/paths/meeting_sections.yml @@ -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': diff --git a/docs/api/apiv3/paths/meeting_sections_by_meeting.yml b/docs/api/apiv3/paths/meeting_sections_by_meeting.yml new file mode 100644 index 00000000000..e54b6956670 --- /dev/null +++ b/docs/api/apiv3/paths/meeting_sections_by_meeting.yml @@ -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 diff --git a/docs/release-notes/17-6-0/README.md b/docs/release-notes/17-6-0/README.md index c08b6b3ce9e..2e7c49049c9 100644 --- a/docs/release-notes/17-6-0/README.md +++ b/docs/release-notes/17-6-0/README.md @@ -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. + diff --git a/modules/meeting/lib/api/v3/meeting_agenda_items/agenda_items_by_meeting_api.rb b/modules/meeting/lib/api/v3/meeting_agenda_items/agenda_items_by_meeting_api.rb index c9b1f484bc5..5fc7f2dcb1a 100644 --- a/modules/meeting/lib/api/v3/meeting_agenda_items/agenda_items_by_meeting_api.rb +++ b/modules/meeting/lib/api/v3/meeting_agenda_items/agenda_items_by_meeting_api.rb @@ -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 diff --git a/modules/meeting/lib/api/v3/meeting_agenda_items/meeting_agenda_item_representer.rb b/modules/meeting/lib/api/v3/meeting_agenda_items/meeting_agenda_item_representer.rb index 16d41286ba6..9a23182b7ab 100644 --- a/modules/meeting/lib/api/v3/meeting_agenda_items/meeting_agenda_item_representer.rb +++ b/modules/meeting/lib/api/v3/meeting_agenda_items/meeting_agenda_item_representer.rb @@ -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 diff --git a/modules/meeting/lib/api/v3/meeting_agenda_items/meeting_agenda_items_api.rb b/modules/meeting/lib/api/v3/meeting_agenda_items/meeting_agenda_items_api.rb new file mode 100644 index 00000000000..a1936b3c0a6 --- /dev/null +++ b/modules/meeting/lib/api/v3/meeting_agenda_items/meeting_agenda_items_api.rb @@ -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 diff --git a/modules/meeting/lib/api/v3/meeting_outcomes/meeting_outcome_representer.rb b/modules/meeting/lib/api/v3/meeting_outcomes/meeting_outcome_representer.rb index 153353f19fe..dac02c36edb 100644 --- a/modules/meeting/lib/api/v3/meeting_outcomes/meeting_outcome_representer.rb +++ b/modules/meeting/lib/api/v3/meeting_outcomes/meeting_outcome_representer.rb @@ -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 diff --git a/modules/meeting/lib/api/v3/meeting_outcomes/meeting_outcomes_api.rb b/modules/meeting/lib/api/v3/meeting_outcomes/meeting_outcomes_api.rb new file mode 100644 index 00000000000..8101c85217f --- /dev/null +++ b/modules/meeting/lib/api/v3/meeting_outcomes/meeting_outcomes_api.rb @@ -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 diff --git a/modules/meeting/lib/api/v3/meeting_outcomes/outcomes_by_agenda_item_api.rb b/modules/meeting/lib/api/v3/meeting_outcomes/outcomes_by_agenda_item_api.rb index 9ab379fad82..298e54d3421 100644 --- a/modules/meeting/lib/api/v3/meeting_outcomes/outcomes_by_agenda_item_api.rb +++ b/modules/meeting/lib/api/v3/meeting_outcomes/outcomes_by_agenda_item_api.rb @@ -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 diff --git a/modules/meeting/lib/api/v3/meeting_sections/meeting_section_representer.rb b/modules/meeting/lib/api/v3/meeting_sections/meeting_section_representer.rb index 79b6c768f62..3ffef01b4fe 100644 --- a/modules/meeting/lib/api/v3/meeting_sections/meeting_section_representer.rb +++ b/modules/meeting/lib/api/v3/meeting_sections/meeting_section_representer.rb @@ -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 diff --git a/modules/meeting/lib/api/v3/meeting_sections/meeting_sections_api.rb b/modules/meeting/lib/api/v3/meeting_sections/meeting_sections_api.rb new file mode 100644 index 00000000000..4434c35ea40 --- /dev/null +++ b/modules/meeting/lib/api/v3/meeting_sections/meeting_sections_api.rb @@ -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 diff --git a/modules/meeting/lib/api/v3/meeting_sections/sections_by_meeting_api.rb b/modules/meeting/lib/api/v3/meeting_sections/sections_by_meeting_api.rb index efe1479a4d6..ace20bb5f02 100644 --- a/modules/meeting/lib/api/v3/meeting_sections/sections_by_meeting_api.rb +++ b/modules/meeting/lib/api/v3/meeting_sections/sections_by_meeting_api.rb @@ -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 diff --git a/modules/meeting/lib/api/v3/meetings/meeting_representer.rb b/modules/meeting/lib/api/v3/meetings/meeting_representer.rb index 11cc02bb724..793490a5a01 100644 --- a/modules/meeting/lib/api/v3/meetings/meeting_representer.rb +++ b/modules/meeting/lib/api/v3/meetings/meeting_representer.rb @@ -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 diff --git a/modules/meeting/lib/api/v3/meetings/meetings_api.rb b/modules/meeting/lib/api/v3/meetings/meetings_api.rb index 34e793394d3..d339b4cc684 100644 --- a/modules/meeting/lib/api/v3/meetings/meetings_api.rb +++ b/modules/meeting/lib/api/v3/meetings/meetings_api.rb @@ -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 diff --git a/modules/meeting/lib/open_project/meeting/engine.rb b/modules/meeting/lib/open_project/meeting/engine.rb index c7894770d41..8ea3f1a9cdc 100644 --- a/modules/meeting/lib/open_project/meeting/engine.rb +++ b/modules/meeting/lib/open_project/meeting/engine.rb @@ -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 diff --git a/modules/meeting/spec/contracts/meeting_sections/create_contract_spec.rb b/modules/meeting/spec/contracts/meeting_sections/create_contract_spec.rb index e088c011516..31465356c6e 100644 --- a/modules/meeting/spec/contracts/meeting_sections/create_contract_spec.rb +++ b/modules/meeting/spec/contracts/meeting_sections/create_contract_spec.rb @@ -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 diff --git a/modules/meeting/spec/contracts/meeting_sections/delete_contract_spec.rb b/modules/meeting/spec/contracts/meeting_sections/delete_contract_spec.rb index dfdcbcefa74..3f25dc9ed4a 100644 --- a/modules/meeting/spec/contracts/meeting_sections/delete_contract_spec.rb +++ b/modules/meeting/spec/contracts/meeting_sections/delete_contract_spec.rb @@ -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 diff --git a/modules/meeting/spec/contracts/meeting_sections/update_contract_spec.rb b/modules/meeting/spec/contracts/meeting_sections/update_contract_spec.rb index 84a3653fbea..a68ebe145bb 100644 --- a/modules/meeting/spec/contracts/meeting_sections/update_contract_spec.rb +++ b/modules/meeting/spec/contracts/meeting_sections/update_contract_spec.rb @@ -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 diff --git a/modules/meeting/spec/requests/api/v3/meeting_agenda_items/agenda_items_by_meeting_resource_spec.rb b/modules/meeting/spec/requests/api/v3/meeting_agenda_items/agenda_items_by_meeting_resource_spec.rb index 5d5bdb39574..c040a31a1ed 100644 --- a/modules/meeting/spec/requests/api/v3/meeting_agenda_items/agenda_items_by_meeting_resource_spec.rb +++ b/modules/meeting/spec/requests/api/v3/meeting_agenda_items/agenda_items_by_meeting_resource_spec.rb @@ -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 } diff --git a/modules/meeting/spec/requests/api/v3/meeting_outcomes/outcomes_by_agenda_item_resource_spec.rb b/modules/meeting/spec/requests/api/v3/meeting_outcomes/outcomes_by_agenda_item_resource_spec.rb index dc0b3d6419a..5dfc4be0954 100644 --- a/modules/meeting/spec/requests/api/v3/meeting_outcomes/outcomes_by_agenda_item_resource_spec.rb +++ b/modules/meeting/spec/requests/api/v3/meeting_outcomes/outcomes_by_agenda_item_resource_spec.rb @@ -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 } diff --git a/modules/meeting/spec/requests/api/v3/meeting_sections/sections_by_meeting_resource_spec.rb b/modules/meeting/spec/requests/api/v3/meeting_sections/sections_by_meeting_resource_spec.rb index 17782fcc872..3cdac775747 100644 --- a/modules/meeting/spec/requests/api/v3/meeting_sections/sections_by_meeting_resource_spec.rb +++ b/modules/meeting/spec/requests/api/v3/meeting_sections/sections_by_meeting_resource_spec.rb @@ -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