diff --git a/docs/api/apiv3/components/schemas/meeting_model.yml b/docs/api/apiv3/components/schemas/meeting_model.yml new file mode 100644 index 000000000000..a35a8bb67b50 --- /dev/null +++ b/docs/api/apiv3/components/schemas/meeting_model.yml @@ -0,0 +1,88 @@ +# Schema: Meeting_PageModel +--- +type: object +required: + - title +properties: + id: + type: integer + description: Identifier of this meeting + readOnly: true + exclusiveMinimum: 0 + title: + type: string + description: The meeting's title + _links: + type: object + properties: + self: + allOf: + - "$ref": "./link.yml" + - description: |- + This meeting + + **Resource**: Meeting + readOnly: true + author: + allOf: + - "$ref": "./link.yml" + - description: |- + The user having created the meeting + + **Resource**: User + readOnly: true + project: + allOf: + - "$ref": "./link.yml" + - description: |- + The project the meeting is in + + **Resource**: Project + attachments: + allOf: + - $ref: './link.yml' + - description: |- + The attachment collection of this grid. + + **Resource**: AttachmentCollection + addAttachment: + allOf: + - "$ref": "./link.yml" + - description: |- + Attach a file to the meeting + + # Conditions + + **Permission**: edit meeting + readOnly: true +example: + _type: Meeting + id: 72 + lockVersion: 5 + title: A meeting + startTime: '2014-05-21T08:00:00.000Z' + endTime: '2014-05-21T10:00:00.000Z' + duration: 120 + createdAt: '2014-05-21T08:51:20.396Z' + updatedAt: '2014-05-21T09:14:02.776Z' + _embedded: + project: + _type: Project... + id: 12 + author: + _type: User... + id: 2 + _links: + addAttachment: + href: "/api/v3/meetings/72/attachments" + method: post + attachments: + href: "/api/v3/meetings/72/attachments" + project: + href: "/api/v3/projects/12" + title: some project + author: + href: "/api/v3/users/2" + title: Peggie Feeney + self: + href: "/api/v3/meetings/72" diff --git a/docs/api/apiv3/openapi-spec.yml b/docs/api/apiv3/openapi-spec.yml index 30ae39bbb872..3fb0874dfef5 100644 --- a/docs/api/apiv3/openapi-spec.yml +++ b/docs/api/apiv3/openapi-spec.yml @@ -220,6 +220,10 @@ paths: "$ref": "./paths/help_texts.yml" "/api/v3/help_texts/{id}": "$ref": "./paths/help_text.yml" + "/api/v3/meetings/{id}": + "$ref": "./paths/meeting.yml" + "/api/v3/meetings/{id}/attachments": + "$ref": "./paths/meeting_attachments.yml" "/api/v3/memberships": "$ref": "./paths/memberships.yml" "/api/v3/memberships/available_projects": @@ -649,6 +653,8 @@ components: "$ref": "./components/schemas/list_of_news_model.yml" List_projects_by_versionModel: "$ref": "./components/schemas/list_projects_by_version_model.yml" + MeetingModel: + "$ref": "./components/schemas/meeting_model.yml" MarkdownModel: "$ref": "./components/schemas/markdown_model.yml" MembershipCollectionModel: diff --git a/docs/api/apiv3/paths/meeting.yml b/docs/api/apiv3/paths/meeting.yml new file mode 100644 index 000000000000..fc052659abcd --- /dev/null +++ b/docs/api/apiv3/paths/meeting.yml @@ -0,0 +1,72 @@ +# /api/v3/meetings/{id} +--- +get: + parameters: + - description: Meeting identifier + example: '1' + in: path + name: id + required: true + schema: + type: integer + responses: + '200': + content: + application/hal+json: + examples: + response: + value: + _embedded: + project: + _type: Project... + id: 12 + author: + _type: User... + id: 2 + _links: + addAttachment: + href: "/api/v3/meetings/72/attachments" + method: post + attachments: + href: "/api/v3/meetings/72/attachments" + project: + href: "/api/v3/projects/12" + title: some project + author: + href: "/api/v3/users/2" + title: Peggie Feeney + self: + href: "/api/v3/meetings/72" + _type: Meeting + id: 72 + lockVersion: 5 + title: A meeting + startTime: '2014-05-21T08:00:00.000Z' + duration: 120 + createdAt: '2014-05-21T08:51:20.396Z' + updatedAt: '2014-05-21T09:14:02.776Z' + schema: + "$ref": "../components/schemas/meeting_model.yml" + description: OK + headers: { } + '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 in the page's project + headers: { } + tags: + - Meetings + description: Retrieve an individual meeting as identified by the id parameter + operationId: View_Meeting + summary: View Meeting Page diff --git a/docs/api/apiv3/paths/meeting_attachments.yml b/docs/api/apiv3/paths/meeting_attachments.yml new file mode 100644 index 000000000000..592b6272d9cd --- /dev/null +++ b/docs/api/apiv3/paths/meeting_attachments.yml @@ -0,0 +1,260 @@ +# /api/v3/meetings/{id}/attachments +--- +get: + parameters: + - description: ID of the meeting whose attachments will be listed + example: '1' + in: path + name: id + required: true + schema: + type: integer + responses: + '200': + content: + application/hal+json: + examples: + response: + value: + _embedded: + elements: + - _links: + author: + href: "/api/v3/users/1" + title: OpenProject Admin + container: + href: "/api/v3/meetings/72" + title: meeting + delete: + href: "/api/v3/attachments/376" + method: delete + downloadLocation: + href: "/api/v3/attachments/376/content" + self: + href: "/api/v3/attachments/376" + title: 200.gif + _type: Attachment + contentType: image/gif + createdAt: '2018-06-01T07:24:19.896Z' + description: + format: plain + html: '' + raw: '' + digest: + algorithm: md5 + hash: 7ac9c97ef73d47127f590788b84c0c1c + fileName: some.gif + fileSize: 3521772 + id: 376 + _links: + self: + href: "/api/v3/meetings/72/attachments" + _type: Collection + count: 1 + total: 1 + schema: + "$ref": "../components/schemas/attachments_model.yml" + description: OK + headers: {} + '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 + + *Note: A client without sufficient permissions shall not be able to test for the existence of a meeting. + That's why a 404 is returned here, even if a 403 might be more appropriate.* + headers: {} + tags: + - Attachments + description: '' + operationId: List_attachments_by_meeting + summary: List attachments by meeting +post: + parameters: + - description: ID of the meeting to receive the attachment + example: '1' + in: path + name: id + required: true + schema: + type: integer + responses: + '200': + content: + application/hal+json: + examples: + response: + value: + _embedded: + author: + _links: + lock: + href: "/api/v3/users/1/lock" + method: post + title: Set lock on admin + self: + href: "/api/v3/users/1" + title: OpenProject Admin + showUser: + href: "/users/1" + type: text/html + updateImmediately: + href: "/api/v3/users/1" + method: patch + title: Update admin + _type: User + admin: true + avatar: '' + createdAt: '2015-03-20T12:56:52.343Z' + email: + firstName: OpenProject + id: 1 + identityUrl: + lastName: Admin + login: admin + name: OpenProject Admin + status: active + updatedAt: '2018-05-29T13:57:44.662Z' + container: + _links: + addAttachment: + href: "/api/v3/meetings/72/attachments" + method: post + attachments: + href: "/api/v3/meetings/72/attachments" + project: + href: "/api/v3/projects/12" + title: Demo project + self: + href: "/api/v3/meetings/72" + _type: Meeting + id: 72 + title: meeting + _links: + author: + href: "/api/v3/users/1" + title: OpenProject Admin + container: + href: "/api/v3/meetings/72" + title: meeting + delete: + href: "/api/v3/attachments/376" + method: delete + downloadLocation: + href: "/api/v3/attachments/376/content" + self: + href: "/api/v3/attachments/376" + title: 200.gif + _type: Attachment + contentType: image/gif + createdAt: '2018-06-01T07:24:19.896Z' + description: + format: plain + html: '' + raw: '' + digest: + algorithm: md5 + hash: 7ac9c97ef73d47127f590788b84c0c1c + fileName: some.gif + fileSize: 3521772 + id: 376 + description: OK + headers: {} + '400': + content: + application/hal+json: + schema: + $ref: "../components/schemas/error_response.yml" + examples: + response: + value: + _type: Error + errorIdentifier: urn:openproject-org:api:v3:errors:InvalidRequestBody + message: The request could not be parsed as JSON. + description: |- + Returned if the client sends a not understandable request. Reasons include: + + * Omitting one of the required parts (metadata and file) + + * sending unparsable JSON in the metadata part + headers: {} + '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 allowed to delete this attachment. + description: |- + Returned if the client does not have sufficient permissions. + + **Required permission:** edit meetings + + *Note that you will only receive this error, if you are at least allowed to see the meeting* + headers: {} + '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 + + *Note: A client without sufficient permissions shall not be able to test for the existence of a meeting + That's why a 404 is returned here, even if a 403 might be more appropriate.* + headers: {} + '406': + $ref: "../components/responses/missing_content_type.yml" + '415': + $ref: "../components/responses/unsupported_media_type.yml" + '422': + content: + application/hal+json: + schema: + $ref: "../components/schemas/error_response.yml" + examples: + response: + value: + _type: Error + errorIdentifier: urn:openproject-org:api:v3:errors:PropertyConstraintViolation + message: File is too large (maximum size is 5242880 Bytes). + description: |- + Returned if the client tries to send an invalid attachment. + Reasons are: + + * Omitting the file name (`fileName` property of metadata part) + + * Sending a file that is too large + headers: {} + tags: + - Attachments + description: |- + Adds an attachment with the meeting as it's container. + + operationId: Add_attachment_to_meeting + summary: Add attachment to meeting diff --git a/frontend/src/app/features/hal/resources/meeting-resource.ts b/frontend/src/app/features/hal/resources/meeting-resource.ts new file mode 100644 index 000000000000..4d2260038394 --- /dev/null +++ b/frontend/src/app/features/hal/resources/meeting-resource.ts @@ -0,0 +1,40 @@ +// -- copyright +// OpenProject is an open source project management software. +// Copyright (C) 2012-2024 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. +//++ + +import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { Attachable } from 'core-app/features/hal/resources/mixins/attachable-mixin'; + +export interface MeetingResourceLinks { + addAttachment(attachment:HalResource):Promise; +} + +class MeetingBaseResource extends HalResource { + public $links:MeetingResourceLinks; +} + +export const MeetingResource = Attachable(MeetingBaseResource); diff --git a/frontend/src/app/features/hal/services/hal-resource.config.ts b/frontend/src/app/features/hal/services/hal-resource.config.ts index 7b20feb79745..c2a26efbe542 100644 --- a/frontend/src/app/features/hal/services/hal-resource.config.ts +++ b/frontend/src/app/features/hal/services/hal-resource.config.ts @@ -56,13 +56,16 @@ import { HalResourceFactoryConfigInterface, HalResourceService, } from 'core-app/features/hal/services/hal-resource.service'; -import { QueryFilterInstanceSchemaResource } from 'core-app/features/hal/resources/query-filter-instance-schema-resource'; +import { + QueryFilterInstanceSchemaResource, +} from 'core-app/features/hal/resources/query-filter-instance-schema-resource'; import { ErrorResource } from 'core-app/features/hal/resources/error-resource'; import { SchemaDependencyResource } from 'core-app/features/hal/resources/schema-dependency-resource'; import { WorkPackageCollectionResource } from 'core-app/features/hal/resources/wp-collection-resource'; import { RelationResource } from 'core-app/features/hal/resources/relation-resource'; import { QueryFilterResource } from 'core-app/features/hal/resources/query-filter-resource'; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource'; +import { MeetingResource } from 'core-app/features/hal/resources/meeting-resource'; const halResourceDefaultConfig:{ [typeName:string]:HalResourceFactoryConfigInterface } = { WorkPackage: { @@ -179,6 +182,9 @@ const halResourceDefaultConfig:{ [typeName:string]:HalResourceFactoryConfigInter WikiPage: { cls: WikiPageResource, }, + Meeting: { + cls: MeetingResource, + }, MeetingContent: { cls: MeetingContentResource, }, diff --git a/frontend/src/app/shared/components/editor/components/ckeditor-augmented-textarea/ckeditor-augmented-textarea.component.ts b/frontend/src/app/shared/components/editor/components/ckeditor-augmented-textarea/ckeditor-augmented-textarea.component.ts index 35b6a8a26497..653d3888f25d 100644 --- a/frontend/src/app/shared/components/editor/components/ckeditor-augmented-textarea/ckeditor-augmented-textarea.component.ts +++ b/frontend/src/app/shared/components/editor/components/ckeditor-augmented-textarea/ckeditor-augmented-textarea.component.ts @@ -70,6 +70,8 @@ export class CkeditorAugmentedTextareaComponent extends UntilDestroyedMixin impl @Input() public editorType:ICKEditorType = 'full'; + @Input() public showAttachments = true; + // Which template to include public element:HTMLElement; diff --git a/frontend/src/app/shared/components/editor/components/ckeditor-augmented-textarea/ckeditor-augmented-textarea.html b/frontend/src/app/shared/components/editor/components/ckeditor-augmented-textarea/ckeditor-augmented-textarea.html index c822b1c3265a..771c286e38fa 100644 --- a/frontend/src/app/shared/components/editor/components/ckeditor-augmented-textarea/ckeditor-augmented-textarea.html +++ b/frontend/src/app/shared/components/editor/components/ckeditor-augmented-textarea/ckeditor-augmented-textarea.html @@ -10,7 +10,7 @@
{{ text.attachments }} diff --git a/lib/primer/open_project/forms/dsl/rich_text_area_input.rb b/lib/primer/open_project/forms/dsl/rich_text_area_input.rb index fa2c1dc46bc3..5cf58d9abd28 100644 --- a/lib/primer/open_project/forms/dsl/rich_text_area_input.rb +++ b/lib/primer/open_project/forms/dsl/rich_text_area_input.rb @@ -7,15 +7,16 @@ module Dsl class RichTextAreaInput < Primer::Forms::Dsl::Input attr_reader :name, :label - def initialize(name:, label:, **system_arguments) + def initialize(name:, label:, rich_text_options:, **system_arguments) @name = name @label = label + @rich_text_options = rich_text_options super(**system_arguments) end def to_component - RichTextArea.new(input: self) + RichTextArea.new(input: self, rich_text_options: @rich_text_options) end def type diff --git a/lib/primer/open_project/forms/rich_text_area.html.erb b/lib/primer/open_project/forms/rich_text_area.html.erb index 3b4747379769..a7b9357ff79b 100644 --- a/lib/primer/open_project/forms/rich_text_area.html.erb +++ b/lib/primer/open_project/forms/rich_text_area.html.erb @@ -3,10 +3,12 @@ <%= builder.text_area(@input.name, **@input.input_arguments) %> <% end %> <%= angular_component_tag 'opce-ckeditor-augmented-textarea', - inputs: { - textareaSelector: "##{builder.field_id(@input.name)}", - macros: 'resource', - turboMode: true - } + inputs: @rich_text_options.reverse_merge( + { + textareaSelector: "##{builder.field_id(@input.name)}", + macros: 'resource', + turboMode: true + } + ) %> <% end %> diff --git a/lib/primer/open_project/forms/rich_text_area.rb b/lib/primer/open_project/forms/rich_text_area.rb index 79a50b7fab98..87f4220f9406 100644 --- a/lib/primer/open_project/forms/rich_text_area.rb +++ b/lib/primer/open_project/forms/rich_text_area.rb @@ -9,9 +9,10 @@ class RichTextArea < Primer::Forms::BaseComponent delegate :builder, :form, to: :@input - def initialize(input:) + def initialize(input:, rich_text_options:) super() @input = input + @rich_text_options = rich_text_options end end end diff --git a/modules/meeting/app/components/meetings/show_component.html.erb b/modules/meeting/app/components/meetings/show_component.html.erb index d2ef57e35ced..51e203c6a62f 100644 --- a/modules/meeting/app/components/meetings/show_component.html.erb +++ b/modules/meeting/app/components/meetings/show_component.html.erb @@ -22,6 +22,11 @@ agenda.with_row do render(MeetingAgendaItems::NewButtonComponent.new(meeting: @meeting)) end + + agenda.with_row do + resource = ::API::V3::Meetings::MeetingRepresenter.new(@meeting, current_user: User.current, embed_links: false) + helpers.list_attachments(resource) + end end end diff --git a/modules/meeting/app/forms/meeting_agenda_item/notes.rb b/modules/meeting/app/forms/meeting_agenda_item/notes.rb index 0b605d8f3ba8..9f4881579d39 100644 --- a/modules/meeting/app/forms/meeting_agenda_item/notes.rb +++ b/modules/meeting/app/forms/meeting_agenda_item/notes.rb @@ -27,15 +27,27 @@ #++ class MeetingAgendaItem::Notes < ApplicationForm + delegate :object, to: :@builder + form do |agenda_item_form| agenda_item_form.rich_text_area( name: :notes, label: MeetingAgendaItem.human_attribute_name(:notes), - disabled: @disabled + disabled: @disabled, + rich_text_options: { + resource:, + } ) end def initialize(disabled: false) @disabled = disabled end + + def resource + return unless object&.meeting + + API::V3::Meetings::MeetingRepresenter + .new(object.meeting, current_user: User.current, embed_links: false) + end end diff --git a/modules/meeting/app/models/meeting.rb b/modules/meeting/app/models/meeting.rb index add973eda076..b2ffac87090f 100644 --- a/modules/meeting/app/models/meeting.rb +++ b/modules/meeting/app/models/meeting.rb @@ -28,6 +28,7 @@ class Meeting < ApplicationRecord include VirtualAttribute + include OpenProject::Journal::AttachmentHelper self.table_name = 'meetings' @@ -59,6 +60,16 @@ class Meeting < ApplicationRecord .merge(Project.allowed_to(args.first || User.current, :view_meetings)) } + acts_as_attachable( + after_remove: :attachments_changed, + order: "#{Attachment.table_name}.file", + add_on_new_permission: :create_meetings, + add_on_persisted_permission: :edit_meetings, + view_permission: :view_meetings, + delete_permission: :edit_meetings, + modification_blocked: ->(*) { false } + ) + acts_as_watchable permission: :view_meetings acts_as_searchable columns: [ diff --git a/modules/meeting/lib/api/v3/attachments/attachments_by_meeting_api.rb b/modules/meeting/lib/api/v3/attachments/attachments_by_meeting_api.rb new file mode 100644 index 000000000000..82ac0c379600 --- /dev/null +++ b/modules/meeting/lib/api/v3/attachments/attachments_by_meeting_api.rb @@ -0,0 +1,56 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 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 Attachments + class AttachmentsByMeetingAPI < ::API::OpenProjectAPI + resources :attachments do + helpers API::V3::Attachments::AttachmentsByContainerAPI::Helpers + + helpers do + def container + @meeting + end + + def get_attachment_self_path + api_v3_paths.attachments_by_meeting_content @meeting.id + end + end + + get &API::V3::Attachments::AttachmentsByContainerAPI.read + post &API::V3::Attachments::AttachmentsByContainerAPI.create + + namespace :prepare do + post &API::V3::Attachments::AttachmentsByContainerAPI.prepare + end + end + end + 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 new file mode 100644 index 000000000000..af49d7dc4f68 --- /dev/null +++ b/modules/meeting/lib/api/v3/meetings/meeting_representer.rb @@ -0,0 +1,83 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 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 Meetings + class MeetingRepresenter < ::API::Decorators::Single + include API::Decorators::LinkedResource + include API::Caching::CachedRepresenter + include API::V3::Attachments::AttachableRepresenterMixin + include API::Decorators::DateProperty + + self_link title_getter: ->(*) { represented.title } + + property :id + property :title + property :location + + property :lock_version, + render_nil: true, + getter: ->(*) { + lock_version.to_i + } + + property :type, + as: :meeting_type, + getter: ->(*) { type } + + date_time_property :start_time + date_time_property :end_time + + property :duration + + associated_resource :author, + v3_path: :user, + representer: ::API::V3::Users::UserRepresenter + + associated_resource :project, + link: ->(*) do + next if represented.project.blank? + + { + href: api_v3_paths.project(represented.project.id), + title: represented.project.name + } + end + + date_time_property :created_at + + date_time_property :updated_at + + def _type + 'Meeting' + end + end + end + end +end diff --git a/modules/meeting/lib/api/v3/meetings/meetings_api.rb b/modules/meeting/lib/api/v3/meetings/meetings_api.rb new file mode 100644 index 000000000000..7f9cd0005188 --- /dev/null +++ b/modules/meeting/lib/api/v3/meetings/meetings_api.rb @@ -0,0 +1,55 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 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 Meetings + class MeetingsAPI < ::API::OpenProjectAPI + resources :meetings do + helpers do + def meeting + MeetingContent.find params[:id] + end + end + + route_param :id, type: Integer, desc: 'Activity ID' do + after_validation do + @meeting = Meeting.visible.find(declared_params[:id]) + end + + get &::API::V3::Utilities::Endpoints::Show + .new(model: ::Meeting) + .mount + + mount ::API::V3::Attachments::AttachmentsByMeetingAPI + end + end + end + end + end +end diff --git a/modules/meeting/lib/open_project/meeting/engine.rb b/modules/meeting/lib/open_project/meeting/engine.rb index 3030589bf398..855c1e68d210 100644 --- a/modules/meeting/lib/open_project/meeting/engine.rb +++ b/modules/meeting/lib/open_project/meeting/engine.rb @@ -161,6 +161,7 @@ class Engine < ::Rails::Engine &::OpenProject::Meeting::Patches::API::WorkPackageRepresenter.extension) add_api_endpoint 'API::V3::Root' do + mount ::API::V3::Meetings::MeetingsAPI mount ::API::V3::Meetings::MeetingContentsAPI end @@ -170,6 +171,10 @@ class Engine < ::Rails::Engine PermittedParams.permit(:search, :meetings) end + add_api_path :meeting do |id| + "#{root}/meetings/#{id}" + end + add_api_path :meeting_content do |id| "#{root}/meeting_contents/#{id}" end @@ -182,6 +187,10 @@ class Engine < ::Rails::Engine meeting_content(id) end + add_api_path :attachments_by_meeting do |id| + "#{meeting(id)}/attachments" + end + add_api_path :attachments_by_meeting_content do |id| "#{meeting_content(id)}/attachments" end diff --git a/modules/meeting/spec/features/structured_meetings/attachment_upload_spec.rb b/modules/meeting/spec/features/structured_meetings/attachment_upload_spec.rb new file mode 100644 index 000000000000..a1cad1c3e48b --- /dev/null +++ b/modules/meeting/spec/features/structured_meetings/attachment_upload_spec.rb @@ -0,0 +1,69 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' +require 'features/page_objects/notification' +require_relative '../../support/pages/structured_meeting/show' + +RSpec.describe 'Upload attachment to meetings', :js do + let(:user) do + create(:user, + member_with_permissions: { project => %i[view_meetings edit_meetings manage_agendas] }) + end + let(:project) { create(:project) } + let(:attachments) { Components::Attachments.new } + let(:image_fixture) { UploadedFile.load_from('spec/fixtures/files/image.png') } + let(:editor) { Components::WysiwygEditor.new '#content', 'opce-ckeditor-augmented-textarea' } + let(:wiki_page_content) { project.wiki.pages.first.text } + + let(:meeting) { create(:structured_meeting, project: project)} + let(:show_page) { Pages::StructuredMeeting::Show.new(meeting) } + + before do + login_as(user) + end + + it 'can upload an image to new and existing meeting agenda item via drag & drop in editor' do + show_page.visit! + + show_page.add_agenda_item(save: false) do + click_link_or_button 'Notes' + end + + # adding an image + editor.drag_attachment image_fixture.path, 'Image uploaded the first time' + + editor.attachments_list.expect_attached('image.png') + editor.wait_until_upload_progress_toaster_cleared + + click_link_or_button 'Save' + expect(page).to have_css('#meeting-agenda-items-list-component img', count: 1) + expect(page).to have_content('Image uploaded the first time') + editor.attachments_list.expect_attached('image.png') + end +end diff --git a/modules/meeting/spec/lib/api/v3/meetings/meeting_representer_spec.rb b/modules/meeting/spec/lib/api/v3/meetings/meeting_representer_spec.rb new file mode 100644 index 000000000000..bf376f6b1932 --- /dev/null +++ b/modules/meeting/spec/lib/api/v3/meetings/meeting_representer_spec.rb @@ -0,0 +1,100 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' + +RSpec.describe API::V3::Meetings::MeetingRepresenter do + include API::V3::Utilities::PathHelper + + shared_let(:project) { create(:project) } + let(:permissions) { [:view_meetings] } + let(:user) do + create(:user, + member_with_permissions: { project => permissions }, + created_at: 1.day.ago, + updated_at: Time.zone.now) + end + let(:meeting) do + create(:meeting, + author: user, + location: 'https://foo.example.com', + project:) + end + + let(:representer) { described_class.new(meeting, current_user: user) } + + describe 'generation' do + subject(:generated) { representer.to_json } + + describe 'self link' do + it_behaves_like 'has a titled link' do + let(:link) { 'self' } + let(:href) { api_v3_paths.meeting(meeting.id) } + let(:title) { meeting.title } + end + end + + it_behaves_like 'has an untitled link' do + let(:link) { :attachments } + let(:href) { api_v3_paths.attachments_by_meeting meeting.id } + end + + it_behaves_like 'has a titled link' do + let(:link) { 'author' } + let(:href) { api_v3_paths.user(user.id) } + let(:title) { user.name } + end + + it_behaves_like 'has a titled link' do + let(:link) { 'project' } + let(:href) { api_v3_paths.project(project.id) } + let(:title) { project.name } + end + + it_behaves_like 'has an untitled action link' do + let(:link) { :addAttachment } + let(:href) { api_v3_paths.attachments_by_meeting meeting.id } + let(:method) { :post } + let(:permission) { :edit_meetings } + end + + it 'describes the object', :aggregate_failures do + expect(subject).to be_json_eql('Meeting'.to_json).at_path('_type') + expect(subject).to be_json_eql(meeting.id.to_json).at_path('id') + expect(subject).to be_json_eql(meeting.title.to_json).at_path('title') + expect(subject).to be_json_eql(meeting.lock_version.to_json).at_path('lockVersion') + expect(subject).to be_json_eql(meeting.start_time.utc.iso8601(3).to_json).at_path('startTime') + expect(subject).to be_json_eql(meeting.end_time.utc.iso8601(3).to_json).at_path('endTime') + expect(subject).to be_json_eql(meeting.duration.to_json).at_path('duration') + expect(subject).to be_json_eql(meeting.location.to_json).at_path('location') + + expect(subject).to be_json_eql(meeting.created_at.utc.iso8601(3).to_json).at_path('createdAt') + expect(subject).to be_json_eql(meeting.updated_at.utc.iso8601(3).to_json).at_path('updatedAt') + end + end +end diff --git a/modules/meeting/spec/lib/api/v3/utilities/path_helper_spec.rb b/modules/meeting/spec/lib/api/v3/utilities/path_helper_spec.rb new file mode 100644 index 000000000000..7f555949b947 --- /dev/null +++ b/modules/meeting/spec/lib/api/v3/utilities/path_helper_spec.rb @@ -0,0 +1,45 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' + +RSpec.describe API::V3::Utilities::PathHelper do + let(:helper) { Class.new.tap { |c| c.extend(described_class) }.api_v3_paths } + + describe '#meeting' do + subject { helper.meeting 42 } + + it { is_expected.to eql('/api/v3/meetings/42') } + end + + describe '#attachments_by_meeting' do + subject { helper.attachments_by_meeting 42 } + + it { is_expected.to eql('/api/v3/meetings/42/attachments') } + end +end diff --git a/modules/meeting/spec/requests/api/v3/attachments/meetings_spec.rb b/modules/meeting/spec/requests/api/v3/attachments/meetings_spec.rb new file mode 100644 index 000000000000..bb76e3f8eb18 --- /dev/null +++ b/modules/meeting/spec/requests/api/v3/attachments/meetings_spec.rb @@ -0,0 +1,42 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' +require 'requests/api/v3/attachments/attachment_resource_shared_examples' + +RSpec.describe "meetings attachments" do + it_behaves_like "an APIv3 attachment resource" do + let(:attachment_type) { :meeting } + + let(:create_permission) { :create_meetings } + let(:read_permission) { :view_meetings } + let(:update_permission) { :edit_meetings } + + shared_let(:meeting) { create(:meeting, project:) } + end +end diff --git a/modules/meeting/spec/requests/api/v3/meetings/meetings_resource_spec.rb b/modules/meeting/spec/requests/api/v3/meetings/meetings_resource_spec.rb new file mode 100644 index 000000000000..c7918ebd3611 --- /dev/null +++ b/modules/meeting/spec/requests/api/v3/meetings/meetings_resource_spec.rb @@ -0,0 +1,86 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' +require 'rack/test' + +RSpec.describe 'API v3 Meeting resource' do + include Rack::Test::Methods + include API::V3::Utilities::PathHelper + + shared_let(:project) { create(:project) } + shared_let(:meeting) { create(:meeting, project:) } + + let(:permissions) { [:view_meetings] } + let(:current_user) do + create(:user, + member_with_permissions: { project => permissions }) + end + + describe 'meetings/:id' do + let(:get_path) { api_v3_paths.meeting meeting.id } + + context 'with logged in user' do + before do + allow(User).to receive(:current).and_return current_user + + get get_path + end + + context 'when valid id' do + it 'returns HTTP 200' do + expect(last_response.status).to eq 200 + end + end + + context 'when valid id, but not visible' do + let(:permissions) { [:view_work_packages] } + + it 'returns HTTP 404' do + expect(last_response.status).to eq 404 + end + end + + context 'when invalid id' do + let(:get_path) { api_v3_paths.budget 'bogus' } + + it_behaves_like 'param validation error' do + let(:id) { 'bogus' } + end + end + end + + context 'with not logged in user' do + before do + get get_path + end + + it_behaves_like 'not found response based on login_required' + end + end +end diff --git a/modules/meeting/spec/support/pages/structured_meeting/show.rb b/modules/meeting/spec/support/pages/structured_meeting/show.rb index 14e43b50cf11..cfd23e03b0bf 100644 --- a/modules/meeting/spec/support/pages/structured_meeting/show.rb +++ b/modules/meeting/spec/support/pages/structured_meeting/show.rb @@ -36,7 +36,7 @@ def expect_empty expect(page).to have_no_css('[id^="meeting-agenda-items-item-component"]') end - def add_agenda_item(type: MeetingAgendaItem, &) + def add_agenda_item(type: MeetingAgendaItem, save: true, &) page.within("#meeting-agenda-items-new-button-component") do click_button I18n.t(:button_add) click_link type.model_name.human @@ -44,7 +44,7 @@ def add_agenda_item(type: MeetingAgendaItem, &) in_agenda_form do yield - click_button 'Save' + click_button('Save') if save end end diff --git a/spec/support/components/wysiwyg/wysiwyg_editor.rb b/spec/support/components/wysiwyg/wysiwyg_editor.rb index 74e20ec81ba4..57ce75c2d460 100644 --- a/spec/support/components/wysiwyg/wysiwyg_editor.rb +++ b/spec/support/components/wysiwyg/wysiwyg_editor.rb @@ -6,10 +6,10 @@ class WysiwygEditor attr_reader :context_selector, :attachments, :attachments_list - def initialize(context = '#content') + def initialize(context = '#content', attachment_list_selector = 'ckeditor-augmented-textarea') @context_selector = context @attachments = ::Components::Attachments.new - @attachments_list = ::Components::AttachmentsList.new("#{context} ckeditor-augmented-textarea") + @attachments_list = ::Components::AttachmentsList.new("#{context} #{attachment_list_selector}") end def container