From 3aa5275567321b2bf7b8ffa82b4d1261abf53eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 8 Dec 2025 14:08:55 +0100 Subject: [PATCH 1/4] Merge pull request #21214 from opf/feat/workflow-work-package Recreate artifact on status change --- ...load_artifact_on_status_changes_service.rb | 134 ++++++++ app/workers/work_packages/workflow_job.rb | 46 +++ config/initializers/subscribe_listeners.rb | 10 +- .../lib/acts/journalized/save_hooks.rb | 1 + ...artifact_on_status_changes_service_spec.rb | 289 ++++++++++++++++++ 5 files changed, 478 insertions(+), 2 deletions(-) create mode 100644 app/services/projects/creation_wizard/reupload_artifact_on_status_changes_service.rb create mode 100644 app/workers/work_packages/workflow_job.rb create mode 100644 spec/services/projects/creation_wizard/reupload_artifact_on_status_changes_service_spec.rb diff --git a/app/services/projects/creation_wizard/reupload_artifact_on_status_changes_service.rb b/app/services/projects/creation_wizard/reupload_artifact_on_status_changes_service.rb new file mode 100644 index 00000000000..288bbc43f2e --- /dev/null +++ b/app/services/projects/creation_wizard/reupload_artifact_on_status_changes_service.rb @@ -0,0 +1,134 @@ +# 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 Projects::CreationWizard + class ReuploadArtifactOnStatusChangesService + include Contracted + include ProjectHelper + include Rails.application.routes.url_helpers + prepend Projects::Concerns::UpdateDemoData + + attr_reader :current_user, :work_package + + delegate :project, to: :work_package + + def initialize(current_user:, work_package:) + @current_user = current_user + @work_package = work_package + end + + def call!(changes:) + return unless OpenProject::FeatureDecisions.project_initiation_active? + return if changes["status_id"].blank? + return unless update_is_artifact_work_package? + + User.execute_as_admin(current_user) do + update_artifact + end + end + + def update_is_artifact_work_package? + project.project_creation_wizard_artifact_work_package_id.to_s == work_package.id.to_s + end + + private + + def update_artifact + call = store_artifact + if call.success? + Rails.logger.debug { "Updated artifact for creation wizard in ##{work_package.id}" } + else + Rails.logger.error("Failed to process artifact change for work package ##{work_package.id}: ##{call.message}") + end + end + + def store_artifact + if store_attachment_locally? + return add_attachment_locally + end + + if project_storage.nil? + return ServiceResult.failure(message: I18n.t("projects.wizard.create_artifact_storage_error")) + end + + upload_artifact_to_storage + end + + def store_attachment_locally? + project.project_creation_wizard_artifact_export_type == "attachment" + end + + def upload_artifact_to_storage + export = create_pdf_export! + + Storages::UploadFileService + .call( + container: work_package, + project_storage:, + file_path: project.project_creation_wizard_artifact_name, + file_data: StringIO.new(export.content), + filename: export.title + ) + end + + def project_storage + return @project_storage if defined?(@project_storage) + + @project_storage = project + .project_storages + .find_by(id: project.project_creation_wizard_artifact_export_storage) + end + + def add_attachment_locally + export = create_pdf_export! + file = OpenProject::Files.create_uploaded_file( + name: export.title, + content_type: export.mime_type, + content: export.content, + binary: true + ) + + attachment = work_package.attachments.create( + author: current_user, + file: + ) + + if attachment.persisted? + ServiceResult.success(result: attachment) + else + ServiceResult.failure(result: attachment, errors: attachment.errors) + end + end + + def create_pdf_export! + Project::PDFExport::ProjectInitiation.new(project).export! + end + end +end diff --git a/app/workers/work_packages/workflow_job.rb b/app/workers/work_packages/workflow_job.rb new file mode 100644 index 00000000000..08365232be5 --- /dev/null +++ b/app/workers/work_packages/workflow_job.rb @@ -0,0 +1,46 @@ +# 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 WorkPackages + class WorkflowJob < ApplicationJob + def perform(journal, changes) + work_package = journal.journable + process_artifact_changes(work_package, changes) + end + + private + + def process_artifact_changes(work_package, changes) + Projects::CreationWizard::ReuploadArtifactOnStatusChangesService + .new(current_user: User.current, work_package:) + .call!(changes:) + end + end +end diff --git a/config/initializers/subscribe_listeners.rb b/config/initializers/subscribe_listeners.rb index abbc096c3d2..5d087000ca6 100644 --- a/config/initializers/subscribe_listeners.rb +++ b/config/initializers/subscribe_listeners.rb @@ -30,13 +30,19 @@ Rails.application.config.after_initialize do OpenProject::Notifications.subscribe(OpenProject::Events::JOURNAL_CREATED) do |payload| + journal = payload[:journal] + send_notifications = payload[:send_notification] + + # A job is scheduled immediately that handles additional workflows on work packages + WorkPackages::WorkflowJob.perform_later(journal, payload[:changes]) if journal.journable_type == "WorkPackage" + # A job is scheduled immediately that creates notifications (in-app if # supported) right away and schedules jobs to be run for mail and digest # mails. Notifications::WorkflowJob .perform_later(:create_notifications, - payload[:journal], - payload[:send_notification]) + journal, + send_notifications) # A job is scheduled for the end of the journal aggregation time. If the # journal still exists with a matching updated_at value (it might be updated diff --git a/lib_static/plugins/acts_as_journalized/lib/acts/journalized/save_hooks.rb b/lib_static/plugins/acts_as_journalized/lib/acts/journalized/save_hooks.rb index 014f7a7aeb5..f307230e5b4 100644 --- a/lib_static/plugins/acts_as_journalized/lib/acts/journalized/save_hooks.rb +++ b/lib_static/plugins/acts_as_journalized/lib/acts/journalized/save_hooks.rb @@ -67,6 +67,7 @@ module Acts::Journalized if create_call.success? && create_call.result OpenProject::Notifications.send(OpenProject::Events::JOURNAL_CREATED, + changes: previous_changes, journal: create_call.result, send_notification: Journal::NotificationConfiguration.active?) end diff --git a/spec/services/projects/creation_wizard/reupload_artifact_on_status_changes_service_spec.rb b/spec/services/projects/creation_wizard/reupload_artifact_on_status_changes_service_spec.rb new file mode 100644 index 00000000000..db9fe6e3c1f --- /dev/null +++ b/spec/services/projects/creation_wizard/reupload_artifact_on_status_changes_service_spec.rb @@ -0,0 +1,289 @@ +# 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. +#++ + +require "spec_helper" + +RSpec.describe Projects::CreationWizard::ReuploadArtifactOnStatusChangesService, + with_flag: { project_initiation: true } do + shared_let(:status_new) { create(:status, name: "New") } + shared_let(:status_approved) { create(:status, name: "Approved") } + shared_let(:type) { create(:type, name: "Project initiation") } + shared_let(:user_custom_field) { create(:user_project_custom_field, name: "Project Manager") } + shared_let(:assignee_user) { create(:user, firstname: "assignee_user") } + shared_let(:current_user) { create(:user, lastname: "current_user") } + shared_let(:role) do + create(:project_role, permissions: %i[ + add_work_packages + view_project_attributes + work_package_assigned + edit_work_packages + ]) + end + shared_let(:default_priority) { create(:default_priority) } + shared_let(:project) do + create( + :project, + name: "Important Project", + types: [type], + project_custom_fields: [user_custom_field], + # project initiation request settings + project_creation_wizard_artifact_name: "project_mandate", + project_creation_wizard_enabled: true, + project_creation_wizard_work_package_type_id: type.id, + project_creation_wizard_status_when_submitted_id: status_new.id, + project_creation_wizard_assignee_custom_field_id: user_custom_field.id, + project_creation_wizard_work_package_comment: "PIR submitted for **Project Name**.", + user_custom_field.attribute_name => assignee_user.id + ).tap do |p| + p.members << create(:member, principal: assignee_user, project: p, roles: [role]) + p.members << create(:member, principal: current_user, project: p, roles: [role]) + end + end + + shared_let(:work_package) do + create( + :work_package, + project:, + type:, + status: status_new, + subject: "Artifact Work Package", + assigned_to: assignee_user + ) + end + + let(:instance) do + described_class.new(current_user:, work_package:) + end + + let(:changes) { { "status_id" => [status_new.id, status_approved.id] } } + + before do + login_as current_user + project.update(project_creation_wizard_artifact_work_package_id: work_package.id) + end + + describe "#call!" do + context "when status_id is not in changes" do + let(:changes) { {} } + + it "returns early without processing" do + project.update(project_creation_wizard_artifact_export_type: "attachment") + allow(User).to receive(:execute_as_admin) + + instance.call!(changes:) + + expect(User).not_to have_received(:execute_as_admin) + end + end + + context "when status_id is blank in changes" do + let(:changes) { { "status_id" => nil } } + + it "returns early without processing" do + project.update(project_creation_wizard_artifact_export_type: "attachment") + allow(User).to receive(:execute_as_admin) + + instance.call!(changes:) + + expect(User).not_to have_received(:execute_as_admin) + end + end + + context "when work package is not the artifact work package" do + before do + project.update(project_creation_wizard_artifact_work_package_id: work_package.id + 999) + end + + it "returns early without processing" do + project.update(project_creation_wizard_artifact_export_type: "attachment") + allow(User).to receive(:execute_as_admin) + + instance.call!(changes:) + + expect(User).not_to have_received(:execute_as_admin) + end + end + + context "when artifact storage is internal (attachment)" do + before do + project.update(project_creation_wizard_artifact_export_type: "attachment") + end + + it "adds the PDF as an attachment to the existing work package" do + initial_attachment_count = work_package.attachments.count + + instance.call!(changes:) + + work_package.reload + expect(work_package.attachments.count).to eq(initial_attachment_count + 1) + + attachment = work_package.attachments.last + date = Date.current.iso8601 + expect(attachment.content_type).to eq "application/pdf" + expect(attachment.filename).to match(/.*_Project_mandate_#{status_new.name}_#{date}_\d+-\d+.pdf/) + expect(attachment.author).to eq(current_user) + end + + context "when work package already has an attachment" do + before do + work_package.attachments.create( + author: current_user, + file: OpenProject::Files.create_uploaded_file( + name: "existing_file.pdf", + content_type: "application/pdf", + content: "old content", + binary: true + ) + ) + end + + it "adds a new attachment without removing the existing one" do + initial_count = work_package.attachments.count + + instance.call!(changes:) + + work_package.reload + expect(work_package.attachments.count).to eq(initial_count + 1) + end + end + end + + context "when artifact storage is project storage (file link)" do + let(:storage) { create(:nextcloud_storage_with_local_connection) } + let(:project_storage) { create(:project_storage, project:, storage:, project_folder_id: "/project_folder") } + let(:service_result) { ServiceResult.success(result: nil) } + + before do + project.update( + project_creation_wizard_artifact_export_type: "file_link", + project_creation_wizard_artifact_export_storage: project_storage.id + ) + + allow(Storages::UploadFileService) + .to receive(:call) + .and_return(service_result) + end + + it "uploads the artifact to the project storage" do + instance.call!(changes:) + + work_package.reload + expect(work_package.attachments.count).to eq(0) + + date = Date.current.iso8601 + expect(Storages::UploadFileService) + .to have_received(:call) + .with(container: work_package, + project_storage:, + file_path: "project_mandate", + filename: /.*_Project_mandate_#{status_new.name}_#{date}_\d+-\d+.pdf/, + file_data: instance_of(StringIO)) + end + + context "when storage upload fails" do + let(:service_result) do + ServiceResult.failure(result: nil).tap do |result| + result.errors.add(:base, "Storage upload failed!") + end + end + + it "logs the failure" do + allow(Rails.logger).to receive(:error) + + instance.call!(changes:) + + expect(Storages::UploadFileService).to have_received(:call) + expect(Rails.logger).to have_received(:error).with(/Failed to process artifact change/) + end + end + + context "when project storage is not configured" do + before do + project.update(project_creation_wizard_artifact_export_storage: nil) + end + + it "logs the failure" do + allow(Rails.logger).to receive(:error) + + instance.call!(changes:) + + expect(Rails.logger).to have_received(:error).with(/Failed to process artifact change/) + end + + it "does not call the storage upload service" do + allow(Rails.logger).to receive(:error) + + instance.call!(changes:) + + expect(Storages::UploadFileService).not_to have_received(:call) + end + end + + context "when project storage does not exist (invalid id)" do + before do + project.update(project_creation_wizard_artifact_export_storage: 99999) + end + + it "logs the failure" do + allow(Rails.logger).to receive(:error) + + instance.call!(changes:) + + expect(Rails.logger).to have_received(:error).with(/Failed to process artifact change/) + end + end + end + + context "when current user executes as admin" do + it "executes the update with admin privileges" do + allow(User).to receive(:execute_as_admin).with(current_user).and_call_original + project.update(project_creation_wizard_artifact_export_type: "attachment") + + instance.call!(changes:) + + expect(User).to have_received(:execute_as_admin).with(current_user) + end + end + + context "when PDF export creation fails" do + let(:pdf_export) { instance_double(Project::PDFExport::ProjectInitiation) } + + before do + project.update(project_creation_wizard_artifact_export_type: "attachment") + allow(Project::PDFExport::ProjectInitiation).to receive(:new).and_return(pdf_export) + allow(pdf_export).to receive(:export!).and_raise(StandardError, "PDF generation failed") + end + + it "raises an error" do + expect { instance.call!(changes:) }.to raise_error(StandardError, "PDF generation failed") + end + end + end +end From d3c64be91539be1c256564d0674ce9743ccfa19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 8 Dec 2025 17:45:53 +0100 Subject: [PATCH 2/4] Remove incorrectly assigned bugfix for 16.6.2 --- docs/release-notes/16-6-2/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/release-notes/16-6-2/README.md b/docs/release-notes/16-6-2/README.md index c3b5fd2900f..e3a6479f65b 100644 --- a/docs/release-notes/16-6-2/README.md +++ b/docs/release-notes/16-6-2/README.md @@ -24,7 +24,6 @@ Below you will find a complete list of all changes and bug fixes. - Bugfix: Error when creating a new work package after the previous one is opened in details view \[[#67980](https://community.openproject.org/wp/67980)\] - Bugfix: OpenID Connect: Claims escaped twice \[[#69079](https://community.openproject.org/wp/69079)\] - Bugfix: Disable editing of sendmail attributes through UI \[[#69577](https://community.openproject.org/wp/69577)\] -- Changed: Restrict editing of user email addresses to superadmins, rather than "Manage user" permissions \[[#69578](https://community.openproject.org/wp/69578)\] From 9ff3e9a7cef150fd1f24637f28761149c675f15c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 8 Dec 2025 20:33:12 +0100 Subject: [PATCH 3/4] Update README.md --- docs/security-and-privacy/statement-on-security/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/security-and-privacy/statement-on-security/README.md b/docs/security-and-privacy/statement-on-security/README.md index 418060bde71..01632b85a44 100644 --- a/docs/security-and-privacy/statement-on-security/README.md +++ b/docs/security-and-privacy/statement-on-security/README.md @@ -62,7 +62,11 @@ You can also [report a vulnerability directly in GitHub](https://github.com/opf/ Please include a description on how to reproduce the issue if possible. Our security team will get your email and will attempt to reproduce and fix the issue as soon as possible. -> **Please note:** OpenProject currently does not offer a bug bounty program. We will do our best to give you the appropriate credits for responsibly disclosing a security vulnerability to us. We will gladly reference your work, name, website on every publication we do related to the security update. +## Bug bounty program + +OpenProject is currently subject of a bug bounty program, kindly sponsored by the European Commission. Please see https://yeswehack.com/programs/openproject for more details. + +Please note that OpenProject does not offer its own bug bounty program. For any security vulnerability you responsibly disclose to it, whether it's through another bug bounty porgram or through our website, we will do our best to give you the appropriate credits for responsibly disclosing a security vulnerability to us. We will gladly reference your work, name, website on every publication we do related to the security update. ## OpenProject security features From 4f02650e9d999ccfd1585d8e4144772ef33626be Mon Sep 17 00:00:00 2001 From: OpenProject Actions CI Date: Tue, 9 Dec 2025 03:32:02 +0000 Subject: [PATCH 4/4] update locales from crowdin [ci skip] --- config/locales/crowdin/ru.yml | 12 ++++++------ modules/documents/config/locales/crowdin/de.yml | 2 +- modules/documents/config/locales/crowdin/ru.yml | 6 +++--- modules/storages/config/locales/crowdin/ru.yml | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/config/locales/crowdin/ru.yml b/config/locales/crowdin/ru.yml index 2f941f5bc6c..0ba943a9827 100644 --- a/config/locales/crowdin/ru.yml +++ b/config/locales/crowdin/ru.yml @@ -459,8 +459,8 @@ ru: placeholder: Поиск портфолио sub_items_html: one: "1 подпункт" - few: "%{count} sub-items" - many: "%{count} sub-items" + few: "%{count} подпункта" + many: "%{count} подпунктов" other: "%{count} подпунктов" lists: active: "Активные портфолио" @@ -2577,7 +2577,7 @@ ru: title: "Напишите внутренние комментарии только небольшой группы" description: " " portfolio_management: - description: Align your projects to your strategic goals by organizing them into portfolios and programs. + description: Приведите Ваши проекты в соответствии с Вашими стратегическими целями, организовав их в портфолио и программы. teaser: title: one: "Остался день для пробного токена %{trial_plan}" @@ -4326,7 +4326,7 @@ ru: setting_emails_salutation: "Адрес пользователя в письмах с" setting_oauth_allow_remapping_of_existing_users: "Разрешить перераспределение существующих пользователей" setting_sendmail_location: "Расположение исполняемого файла sendmail" - setting_sendmail_arguments: "Arguments for sendmail" + setting_sendmail_arguments: "Аргументы для sendmail" setting_smtp_enable_starttls_auto: "Автоматически использовать STARTTLS, если таковые имеются" setting_smtp_ssl: "Использовать SSL-подключение" setting_smtp_address: "SMTP сервер" @@ -4490,8 +4490,8 @@ ru: setting_working_days: "Рабочие дни" settings: errors: - not_writable: "The setting is not writable and can only be changed by a sysadmin." - failed_to_update: "Setting '%{name}' could not be updated: %{message}" + not_writable: "Эта настройка недоступна для записи и может быть изменена только администратором." + failed_to_update: "Настройка '%{name}' не может быть обновлена: %{message}" authentication: login: "Входное имя" registration: "Регистрация" diff --git a/modules/documents/config/locales/crowdin/de.yml b/modules/documents/config/locales/crowdin/de.yml index fe38dbc50e8..8ddcaf3e79a 100644 --- a/modules/documents/config/locales/crowdin/de.yml +++ b/modules/documents/config/locales/crowdin/de.yml @@ -82,7 +82,7 @@ de: all: "Alle Dokumente" types: "Typen" collaboration_settings: "Echtzeit-Kollaboration" - last_updated_at: "Zuletzt aktualisiert %{time}." + last_updated_at: "Zuletzt gespeichert %{time}." active_editors: "Aktive Bearbeiter" active_editors_count: one: "1 active editor" diff --git a/modules/documents/config/locales/crowdin/ru.yml b/modules/documents/config/locales/crowdin/ru.yml index 01829e8faec..ab2c349342e 100644 --- a/modules/documents/config/locales/crowdin/ru.yml +++ b/modules/documents/config/locales/crowdin/ru.yml @@ -87,9 +87,9 @@ ru: active_editors: "Активные редакторы" active_editors_count: one: "1 active editor" - few: "%{count} active editors" - many: "%{count} active editors" - other: "%{count} active editors" + few: "%{count} активных редактора" + many: "%{count} активных редакторов" + other: "%{count} активных редакторов" label_attachment_author: "Автор вложения" label_categories: "Категории" new_category: "Новая категория" diff --git a/modules/storages/config/locales/crowdin/ru.yml b/modules/storages/config/locales/crowdin/ru.yml index b847f82e7aa..0b935d5bb4f 100644 --- a/modules/storages/config/locales/crowdin/ru.yml +++ b/modules/storages/config/locales/crowdin/ru.yml @@ -242,22 +242,22 @@ ru: error_invalid_provider_type: Пожалуйста, выберите допустимого поставщика хранилища. file_storage_view: access_management: - automatic_management: Enable automatically-managed access and folders + automatic_management: Включить автоматический доступ и папки automatic_management_description: Each project will be able to decide, when a storage is added to it, whether they want the automatically-managed or the manual approach to folder and access management. description: OpenProject can automatically create and manage project folders when a file storage is added to a project. This can result in a more organized folder structure and straightforward access management that guarantees access to all relevant users. manual_management: Only allow manually-managed access and folders manual_management_description: Projects using this storage will not be offered the option of the automatically-managed approach. Folders and access must be managed manually. setup_incomplete: Выберите тип управления доступом пользователя и созданием папок. - subtitle: Folder and access management + subtitle: Управление папками и доступом access_management_section: Доступ и папки проекта automatically_managed_folders: Автоматически управляемые папки general_information: Информация общего характера oauth_configuration: Конфигурация OAuth one_drive: access_management: - automatic_management: Enable automatically-managed access and folders + automatic_management: Включить автоматический доступ и папки automatic_management_description: Each project using this file storage will necessarily have to use the automatically-managed approach to folder and access management. They cannot do it manually. - manual_management: Only allow manually-managed access and folders + manual_management: Разрешить только ручной управляемый доступ и папки manual_management_description: Projects using this storage will not be offered the option of the automatically-managed approach. Folders and access must be managed manually. one_drive_oauth: Azure OAuth openproject_oauth: OpenProject OAuth