From 84cafdbe072b2d97707a24a752e22f200d5ba54d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 29 Jun 2021 20:23:32 +0200 Subject: [PATCH 01/68] Add setting for whitelist --- .../attachments_settings_controller.rb | 47 ++++++++++++++++++ app/helpers/settings_helper.rb | 5 ++ .../attachments_settings/show.html.erb | 48 +++++++++++++++++++ .../settings/general_settings/show.html.erb | 3 -- config/locales/en.yml | 14 ++++++ config/settings.yml | 5 +- ...r.rb => attachment_parsing_representer.rb} | 0 7 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 app/controllers/admin/settings/attachments_settings_controller.rb create mode 100644 app/views/admin/settings/attachments_settings/show.html.erb rename lib/api/v3/attachments/{attachment_metadata_representer.rb => attachment_parsing_representer.rb} (100%) diff --git a/app/controllers/admin/settings/attachments_settings_controller.rb b/app/controllers/admin/settings/attachments_settings_controller.rb new file mode 100644 index 0000000000..fcd768575d --- /dev/null +++ b/app/controllers/admin/settings/attachments_settings_controller.rb @@ -0,0 +1,47 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Admin::Settings + class AttachmentsSettingsController < ::Admin::SettingsController + menu_item :settings_attachments + + def default_breadcrumb + t(:'attributes.attachments') + end + + private + + def settings_params + super.tap do |settings| + settings["attachment_whitelist"] = settings["attachment_whitelist"].split(/\r?\n/) + end + end + end +end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 94cca1dbbd..ff50fb1035 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -51,6 +51,11 @@ module SettingsHelper controller: '/admin/settings/projects_settings', label: :label_project_plural }, + { + name: 'attachments', + controller: '/admin/settings/attachments_settings', + label: :'attributes.attachments' + }, { name: 'api', controller: '/admin/settings/api_settings', diff --git a/app/views/admin/settings/attachments_settings/show.html.erb b/app/views/admin/settings/attachments_settings/show.html.erb new file mode 100644 index 0000000000..f31ac9d499 --- /dev/null +++ b/app/views/admin/settings/attachments_settings/show.html.erb @@ -0,0 +1,48 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. + +++#%> +<%= toolbar title: t(:'attributes.attachments') %> + +<%= styled_form_tag(admin_settings_update_attachments_path, method: :patch) do %> +
+
+ <%= setting_text_field :attachment_max_size, size: 6, unit: t(:"number.human.storage_units.units.kb"), container_class: '-slim' %> +
+
+ <%= setting_text_area :attachment_whitelist, rows: 5, container_class: '-wide' %> +
+ <%= t('settings.attachments.whitelist_text_html', + ext_example: '*.jpg', + mime_example: 'image/jpeg', + ) %> +
+
+
+ + <%= styled_button_tag t(:button_save), class: '-highlight -with-icon icon-checkmark' %> +<% end %> diff --git a/app/views/admin/settings/general_settings/show.html.erb b/app/views/admin/settings/general_settings/show.html.erb index 2f14fed50c..a55b99690c 100644 --- a/app/views/admin/settings/general_settings/show.html.erb +++ b/app/views/admin/settings/general_settings/show.html.erb @@ -31,9 +31,6 @@ See docs/COPYRIGHT.rdoc for more details. <%= styled_form_tag(admin_settings_update_general_path, method: :patch) do %>
<%= setting_text_field :app_title, size: 30, container_class: '-middle' %>
-
- <%= setting_text_field :attachment_max_size, size: 6, unit: t(:"number.human.storage_units.units.kb"), container_class: '-slim' %> -
<%= setting_text_field :per_page_options, size: 20, container_class: '-slim' %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 5535963207..0d039307f1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2328,6 +2328,7 @@ en: If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Email delivery method" setting_sendmail_location: "Location of the sendmail executable" setting_smtp_enable_starttls_auto: "Automatically use STARTTLS if available" @@ -2443,6 +2444,19 @@ en: passwords: "Passwords" session: "Session" brute_force_prevention: "Automated user blocking" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files. +
+ Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}). +
+ Leave empty to allow any file type to be uploaded. + Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) + will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, diff --git a/config/settings.yml b/config/settings.yml index 3a46d7cd54..c5fcedbf9e 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -326,8 +326,6 @@ session_ttl_enabled: session_ttl: format: int default: 120 -default_notification_option: - default: 'only_my_events' emails_header: serialized: true default: @@ -381,3 +379,6 @@ apiv3_cors_enabled: apiv3_cors_origins: serialized: true default: [] +attachment_whitelist: + serialized: true + default: [] diff --git a/lib/api/v3/attachments/attachment_metadata_representer.rb b/lib/api/v3/attachments/attachment_parsing_representer.rb similarity index 100% rename from lib/api/v3/attachments/attachment_metadata_representer.rb rename to lib/api/v3/attachments/attachment_parsing_representer.rb From c59c57ad765a721fd396722b1e31f24576e88c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 29 Jun 2021 20:23:57 +0200 Subject: [PATCH 02/68] Make attachments API BaseServices compatible --- app/contracts/attachments/create_contract.rb | 66 ++++++++++ app/services/attachments/create_service.rb | 87 ++++++------- .../attachments/set_attributes_service.rb | 37 ++++++ lib/api/utilities/endpoints/bodied.rb | 11 +- lib/api/utilities/endpoints/create.rb | 2 +- .../attachment_parsing_representer.rb | 43 ++++--- lib/api/v3/attachments/attachments_api.rb | 17 +-- .../attachments_by_container_api.rb | 120 +++++------------- 8 files changed, 212 insertions(+), 171 deletions(-) create mode 100644 app/contracts/attachments/create_contract.rb create mode 100644 app/services/attachments/set_attributes_service.rb diff --git a/app/contracts/attachments/create_contract.rb b/app/contracts/attachments/create_contract.rb new file mode 100644 index 0000000000..d8a23e7c22 --- /dev/null +++ b/app/contracts/attachments/create_contract.rb @@ -0,0 +1,66 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Attachments + class CreateContract < ::ModelContract + attribute :file + attribute :filename + attribute :filesize + attribute :digest + attribute :description + attribute :content_type + attribute :container + attribute :author + + validate :validate_attachments_addable + validate :validate_container_addable + validate :validate_author + + private + + def validate_attachments_addable + if Redmine::Acts::Attachable.attachables.none?(&:attachments_addable?) + errors.add(:base, :error_unauthorized) + end + end + + def validate_author + unless model.author == user + errors.add(:author, :invalid) + end + end + + def validate_container_addable + return unless model.container + + errors.add(:base, :error_unauthorized) unless model.container.attachments_addable?(user) + end + end +end diff --git a/app/services/attachments/create_service.rb b/app/services/attachments/create_service.rb index 0671dc65a2..772b5ef3f2 100644 --- a/app/services/attachments/create_service.rb +++ b/app/services/attachments/create_service.rb @@ -26,78 +26,63 @@ # See docs/COPYRIGHT.rdoc for more details. #++ -class Attachments::CreateService +class Attachments::CreateService < ::BaseServices::Create include Attachments::TouchContainer - attr_reader :container, :author + around_call :error_wrapped_call - def initialize(container, author:) - @container = container - @author = author - end - - ## - # Adds and saves the uploaded file as attachment of the given container. - # In case the container supports it, a journal will be written. - # - # An ActiveRecord::RecordInvalid error is raised if any record can't be saved. - def call(uploaded_file:, description:) - create_attachment(uploaded_file, description) - .tap(&method(:created_event)) - end - - private - - def create_attachment(uploaded_file, description) - if container.nil? - save_attachment(uploaded_file, description) - elsif container.class.journaled? - create_journalized(uploaded_file, description) + def persist(call) + attachment = call.result + if attachment.container + in_container_mutex(attachment.container) { super } else - create_unjournalized(uploaded_file, description) + super end end - def create_journalized(uploaded_file, description) + def in_container_mutex(container) OpenProject::Mutex.with_advisory_lock_transaction(container) do - save_attachment(uploaded_file, description).tap do + yield.tap do # Get the latest attachments to ensure having them all for journalization. # We just created an attachment and a different worker might have added attachments # in the meantime, e.g when bulk uploading. container.attachments.reload - - touch(container) end end end - def create_unjournalized(uploaded_file, description) - save_attachment(uploaded_file, description).tap do - touch(container) - end - end - - def save_attachment(uploaded_file, description) - attachment = Attachment.new(file: uploaded_file, - container: container, - description: description, - author: author) - - attachment.save! - attachment - end + def after_perform(call) + attachment = call.result + container = attachment.container - def build_attachment(uploaded_file, description) - container.attachments.build(file: uploaded_file, - description: description, - content_type: uploaded_file.content_type, - author: author) - end + touch(container) unless container.nil? - def created_event(attachment) OpenProject::Notifications.send( OpenProject::Events::ATTACHMENT_CREATED, attachment: attachment ) + + call + end + + def error_wrapped_call + yield + rescue StandardError => e + binding.pry + log_attachment_saving_error(e) + + message = + if e&.class&.to_s == 'Errno::EACCES' + I18n.t('api_v3.errors.unable_to_create_attachment_permissions') + else + I18n.t('api_v3.errors.unable_to_create_attachment') + end + raise message + end + + def log_attachment_saving_error(error) + message = "Failed to save attachment: #{error&.class} - #{error&.message || 'Unknown error'}" + + OpenProject.logger.error message end end diff --git a/app/services/attachments/set_attributes_service.rb b/app/services/attachments/set_attributes_service.rb new file mode 100644 index 0000000000..334ada1fac --- /dev/null +++ b/app/services/attachments/set_attributes_service.rb @@ -0,0 +1,37 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Attachments + class SetAttributesService < ::BaseServices::SetAttributes + def set_default_attributes(params) + model.author = user if model.author.nil? + end + end +end diff --git a/lib/api/utilities/endpoints/bodied.rb b/lib/api/utilities/endpoints/bodied.rb index c5e2ca2697..39494f0eea 100644 --- a/lib/api/utilities/endpoints/bodied.rb +++ b/lib/api/utilities/endpoints/bodied.rb @@ -46,10 +46,17 @@ module API end end + def default_params_getter + ->(request) do + request.request_body + end + end + def initialize(model:, api_name: model.name.demodulize, instance_generator: default_instance_generator(model), params_modifier: default_params_modifier, + params_getter: default_params_getter, process_state: default_process_state, parse_representer: nil, render_representer: nil, @@ -60,6 +67,7 @@ module API self.api_name = api_name self.instance_generator = instance_generator self.params_modifier = params_modifier + self.params_getter = params_getter self.process_state = process_state self.parse_representer = parse_representer || deduce_parse_representer self.render_representer = render_representer || deduce_render_representer @@ -86,7 +94,7 @@ module API .new(request.current_user, model: model, representer: parse_representer) - .call(request.request_body) + .call(params_getter.call(request)) .result end @@ -121,6 +129,7 @@ module API :instance_generator, :parse_representer, :render_representer, + :params_getter, :params_modifier, :process_contract, :process_service, diff --git a/lib/api/utilities/endpoints/create.rb b/lib/api/utilities/endpoints/create.rb index d5bc386c05..67714002f1 100644 --- a/lib/api/utilities/endpoints/create.rb +++ b/lib/api/utilities/endpoints/create.rb @@ -29,7 +29,7 @@ module API module Utilities module Endpoints - class Create < Modify + class Create < Modify def default_instance_generator(_model) ->(_params) do end diff --git a/lib/api/v3/attachments/attachment_parsing_representer.rb b/lib/api/v3/attachments/attachment_parsing_representer.rb index 323bf60e69..34c3735489 100644 --- a/lib/api/v3/attachments/attachment_parsing_representer.rb +++ b/lib/api/v3/attachments/attachment_parsing_representer.rb @@ -34,22 +34,35 @@ require 'roar/json/hal' module API module V3 module Attachments - class AttachmentMetadataRepresenter < ::API::Decorators::Single - def initialize(attachment) - super(attachment, current_user: nil) - end + class AttachmentParsingRepresenter < ::API::Decorators::Single + property :file, + setter: ->(fragment:, **) { + self.file = OpenProject::Files.build_uploaded_file fragment[:tempfile], + fragment[:type] + } + + nested :metadata do + property :filename, + as: :fileName + + property :description, + getter: ->(*) { + ::API::Decorators::Formattable.new(description, plain: true) + }, + setter: ->(fragment:, **) { self.description = fragment['raw'] }, + render_nil: true - property :file_name - property :description, - getter: ->(*) { - ::API::Decorators::Formattable.new(description, plain: true) - }, - setter: ->(fragment:, **) { self.description = fragment['raw'] }, - render_nil: true - - property :content_type, render_nil: false - property :file_size, render_nil: false - property :digest, render_nil: false + property :content_type, + as: :contentType, + render_nil: false + + property :filesize, + as: :fileSize, + render_nil: false + + property :digest, + render_nil: false + end end end end diff --git a/lib/api/v3/attachments/attachments_api.rb b/lib/api/v3/attachments/attachments_api.rb index e4cb927667..f2a1bc8eb8 100644 --- a/lib/api/v3/attachments/attachments_api.rb +++ b/lib/api/v3/attachments/attachments_api.rb @@ -39,25 +39,12 @@ module API def container nil end - - def check_attachments_addable - raise API::Errors::Unauthorized if Redmine::Acts::Attachable.attachables.none?(&:attachments_addable?) - end end - post do - check_attachments_addable - - ::API::V3::Attachments::AttachmentRepresenter.new(parse_and_create, current_user: current_user) - end + post &API::V3::Attachments::AttachmentsByContainerAPI.create namespace :prepare do - post do - require_direct_uploads - check_attachments_addable - - ::API::V3::Attachments::AttachmentUploadRepresenter.new(parse_and_prepare, current_user: current_user) - end + post &API::V3::Attachments::AttachmentsByContainerAPI.prepare end route_param :id, type: Integer, desc: 'Attachment ID' do diff --git a/lib/api/v3/attachments/attachments_by_container_api.rb b/lib/api/v3/attachments/attachments_by_container_api.rb index 745096e05f..089be45c48 100644 --- a/lib/api/v3/attachments/attachments_by_container_api.rb +++ b/lib/api/v3/attachments/attachments_by_container_api.rb @@ -47,39 +47,14 @@ module API request.env['REQUEST_METHOD'] == 'POST' end - def parse_metadata(json) - return nil unless json - - metadata = OpenStruct.new - ::API::V3::Attachments::AttachmentMetadataRepresenter.new(metadata).from_json(json) - - unless metadata.file_name - raise ::API::Errors::Validation.new( - :file_name, - "fileName #{I18n.t('activerecord.errors.messages.blank')}" - ) - end - - metadata - end - def parse_and_prepare - metadata = parse_metadata params[:metadata] + metadata = nil unless metadata raise ::API::Errors::InvalidRequestBody.new(I18n.t('api_v3.errors.multipart_body_error')) end - unless metadata.file_size - raise ::API::Errors::Validation.new( - :file_size, - "fileSize #{I18n.t('activerecord.errors.messages.blank')}" - ) - end - - with_handled_create_errors do - create_attachment metadata - end + create_attachment metadata end def create_attachment(metadata) @@ -92,36 +67,11 @@ module API ) end - def parse_and_create - metadata = parse_metadata params[:metadata] - file = params[:file] - - unless metadata && file - raise ::API::Errors::InvalidRequestBody.new(I18n.t('api_v3.errors.multipart_body_error')) - end - - build_and_attach(metadata, file) - end - - def build_and_attach(metadata, file) - uploaded_file = OpenProject::Files.build_uploaded_file file[:tempfile], - file[:type], - file_name: metadata.file_name.to_s - - service = ::Attachments::CreateService.new(container, author: current_user) - - with_handled_create_errors do - service.call uploaded_file: uploaded_file, - description: metadata.description - end - end - - def check_permissions(permissions) - if permissions.empty? - raise API::Errors::Unauthorized unless container.attachments_addable?(current_user) - else - authorize_any(permissions, projects: container.project) - end + ## + # Additionally to what would be checked by the contract, + # we need to restrict permissions in some use cases of the mounts of this endpoint. + def restrict_permissions(permissions) + authorize_any(permissions, projects: container.project) unless permissions.empty? end def require_direct_uploads @@ -130,30 +80,10 @@ module API end end - def with_handled_create_errors - yield - rescue ActiveRecord::RecordInvalid => e - raise ::API::Errors::ErrorBase.create_and_merge_errors(e.record.errors) - rescue StandardError => e - log_attachment_saving_error(e) - message = - if e&.class&.to_s == 'Errno::EACCES' - I18n.t('api_v3.errors.unable_to_create_attachment_permissions') - else - I18n.t('api_v3.errors.unable_to_create_attachment') - end - raise ::API::Errors::InternalError.new(message) - end - - def log_attachment_saving_error(error) - container_message = if container - "on #{container.class} with ID #{container.id}" - else - "without container" - end - message = "Failed to save attachment #{container_message}: #{error&.class} - #{error&.message || 'Unknown error'}" - - Rails.logger.error message + def parse_multipart(request) + request.params.tap do |params| + params[:metadata] = JSON.parse(params[:metadata]) if params.key?(:metadata) + end end end @@ -168,19 +98,33 @@ module API def self.create(permissions = []) -> do - check_permissions permissions - - ::API::V3::Attachments::AttachmentRepresenter.new(parse_and_create, - current_user: current_user) + restrict_permissions permissions + + instance_exec &::API::V3::Utilities::Endpoints::Create + .new(model: ::Attachment, + parse_representer: AttachmentParsingRepresenter, + params_getter: method(:parse_multipart), + params_modifier: ->(params) do + params.merge(container: container) + end) + .mount end end def self.prepare(permissions = []) -> do require_direct_uploads - check_permissions permissions - - ::API::V3::Attachments::AttachmentUploadRepresenter.new(parse_and_prepare, current_user: current_user) + restrict_permissions permissions + + instance_exec &::API::V3::Utilities::Endpoints::Create + .new(model: ::Attachment, + parse_representer: AttachmentParsingRepresenter, + process_service: nil, # TODO prepare service + params_getter: ->(request) { request.params }, + params_modifier: ->(params) do + params.merge(container: container) + end) + .mount end end end From 287111c574c0553f93412e79a972d871cd5da556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Fri, 2 Jul 2021 21:04:59 +0200 Subject: [PATCH 03/68] Add prepare service and contract --- app/contracts/attachments/create_contract.rb | 1 + .../attachments/prepare_upload_contract.rb | 43 +++++++++++++ app/models/attachment.rb | 35 +--------- app/services/attachments/create_service.rb | 1 - .../attachments/prepare_upload_service.rb | 64 +++++++++++++++++++ config/locales/en.yml | 1 + .../attachments_by_container_api.rb | 32 +--------- 7 files changed, 114 insertions(+), 63 deletions(-) create mode 100644 app/contracts/attachments/prepare_upload_contract.rb create mode 100644 app/services/attachments/prepare_upload_service.rb diff --git a/app/contracts/attachments/create_contract.rb b/app/contracts/attachments/create_contract.rb index d8a23e7c22..f7eed10e0b 100644 --- a/app/contracts/attachments/create_contract.rb +++ b/app/contracts/attachments/create_contract.rb @@ -37,6 +37,7 @@ module Attachments attribute :description attribute :content_type attribute :container + attribute :container_type attribute :author validate :validate_attachments_addable diff --git a/app/contracts/attachments/prepare_upload_contract.rb b/app/contracts/attachments/prepare_upload_contract.rb new file mode 100644 index 0000000000..456c2eb0ab --- /dev/null +++ b/app/contracts/attachments/prepare_upload_contract.rb @@ -0,0 +1,43 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Attachments + class PrepareUploadContract < CreateContract + validate :validate_direct_uploads_active + # prepared uploads require a filesize to be present + validates :filesize, presence: true + + private + + def validate_direct_uploads_active + errors.add :base, :not_available unless OpenProject::Configuration.direct_uploads? + end + end +end diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 15f32c7810..5d24495e59 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -47,8 +47,8 @@ class Attachment < ApplicationRecord acts_as_journalized acts_as_event title: -> { file.name }, url: (Proc.new do |o| - { controller: '/attachments', action: 'download', id: o.id, filename: o.filename } - end) + { controller: '/attachments', action: 'download', id: o.id, filename: o.filename } + end) mount_uploader :file, OpenProject::Configuration.file_uploader @@ -255,37 +255,6 @@ class Attachment < ApplicationRecord where(digest: "", downloads: -1) end - def self.create_pending_direct_upload(file_name:, author:, container: nil, content_type: nil, file_size: 0) - a = create( - container: container, - author: author, - content_type: content_type.presence || "application/octet-stream", - filesize: file_size, - digest: "", - downloads: -1 - ) - - # We need to do it like this because `file` is an uploader which expects a File (not a string) - # to upload usually. But in this case the data has already been uploaded and we just point to it. - a[:file] = pending_direct_upload_filename(file_name) - - a.save! - a.reload # necessary so that the fog file uploader path is correct - - a - end - - class << self - private - - # The name has to be in the same format as what Carrierwave will produce later on. If they are different, - # Carrierwave will alter the name (both local and remote) whenever the attachment is saved with the remote - # file loaded. - def pending_direct_upload_filename(file_name) - CarrierWave::SanitizedFile.new(nil).send(:sanitize, file_name) - end - end - def pending_direct_upload? digest == "" && downloads == -1 end diff --git a/app/services/attachments/create_service.rb b/app/services/attachments/create_service.rb index 772b5ef3f2..33f00f1586 100644 --- a/app/services/attachments/create_service.rb +++ b/app/services/attachments/create_service.rb @@ -68,7 +68,6 @@ class Attachments::CreateService < ::BaseServices::Create def error_wrapped_call yield rescue StandardError => e - binding.pry log_attachment_saving_error(e) message = diff --git a/app/services/attachments/prepare_upload_service.rb b/app/services/attachments/prepare_upload_service.rb new file mode 100644 index 0000000000..0f7a9546d6 --- /dev/null +++ b/app/services/attachments/prepare_upload_service.rb @@ -0,0 +1,64 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +class Attachments::PrepareUploadService < ::BaseServices::Create + def instance(params) + binding.pry + super(params).tap do |attachment| + # We need to do it like this because `file` is an uploader which expects a File (not a string) + # to upload usually. But in this case the data has already been uploaded and we just point to it. + attachment[:file] = pending_direct_upload_filename(params[:filename]) + + attachment.extend(OpenProject::ChangedBySystem) + attachment.change_by_system do + attachment.downloads = -1 + attachment.content_type = params[:content_type] || 'application/octet-stream' + end + end + end + + def persist(call) + attachment = call.result + + if attachment.save + attachment.reload # necessary so that the fog file uploader path is correct + ServiceResult.new success: true, result: attachment + else + ServiceResult.new success: false, result: attachment + end + end + + private + + # The name has to be in the same format as what Carrierwave will produce later on. If they are different, + # Carrierwave will alter the name (both local and remote) whenever the attachment is saved with the remote + # file loaded. + def pending_direct_upload_filename(filename) + CarrierWave::SanitizedFile.new(nil).send(:sanitize, filename) + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 0d039307f1..d5f1142819 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -617,6 +617,7 @@ en: invalid_url: 'is not a valid URL.' invalid_url_scheme: 'is not a supported protocol (allowed: %{allowed_schemes}).' less_than_or_equal_to: "must be less than or equal to %{count}." + not_available: "is not available due to a system configuration." not_deletable: "cannot be deleted." not_current_user: "is not the current user." not_a_date: "is not a valid date." diff --git a/lib/api/v3/attachments/attachments_by_container_api.rb b/lib/api/v3/attachments/attachments_by_container_api.rb index 089be45c48..c9b76afede 100644 --- a/lib/api/v3/attachments/attachments_by_container_api.rb +++ b/lib/api/v3/attachments/attachments_by_container_api.rb @@ -47,26 +47,6 @@ module API request.env['REQUEST_METHOD'] == 'POST' end - def parse_and_prepare - metadata = nil - - unless metadata - raise ::API::Errors::InvalidRequestBody.new(I18n.t('api_v3.errors.multipart_body_error')) - end - - create_attachment metadata - end - - def create_attachment(metadata) - Attachment.create_pending_direct_upload( - file_name: metadata.file_name, - container: container, - author: current_user, - content_type: metadata.content_type, - file_size: metadata.file_size - ) - end - ## # Additionally to what would be checked by the contract, # we need to restrict permissions in some use cases of the mounts of this endpoint. @@ -74,12 +54,6 @@ module API authorize_any(permissions, projects: container.project) unless permissions.empty? end - def require_direct_uploads - unless OpenProject::Configuration.direct_uploads? - raise API::Errors::NotFound, message: "Only available if direct uploads are enabled." - end - end - def parse_multipart(request) request.params.tap do |params| params[:metadata] = JSON.parse(params[:metadata]) if params.key?(:metadata) @@ -113,14 +87,14 @@ module API def self.prepare(permissions = []) -> do - require_direct_uploads restrict_permissions permissions instance_exec &::API::V3::Utilities::Endpoints::Create .new(model: ::Attachment, parse_representer: AttachmentParsingRepresenter, - process_service: nil, # TODO prepare service - params_getter: ->(request) { request.params }, + process_service: ::Attachments::PrepareUploadService, + process_contract: ::Attachments::PrepareUploadContract, + params_getter: method(:parse_multipart), params_modifier: ->(params) do params.merge(container: container) end) From 0e8f96bb1851413d259eb60392bd84b772a4a0df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Fri, 2 Jul 2021 21:11:31 +0200 Subject: [PATCH 04/68] Correctly pass the filename to the UploadedFile --- .../attachments/attachment_parsing_representer.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/api/v3/attachments/attachment_parsing_representer.rb b/lib/api/v3/attachments/attachment_parsing_representer.rb index 34c3735489..563d496ce0 100644 --- a/lib/api/v3/attachments/attachment_parsing_representer.rb +++ b/lib/api/v3/attachments/attachment_parsing_representer.rb @@ -35,12 +35,6 @@ module API module V3 module Attachments class AttachmentParsingRepresenter < ::API::Decorators::Single - property :file, - setter: ->(fragment:, **) { - self.file = OpenProject::Files.build_uploaded_file fragment[:tempfile], - fragment[:type] - } - nested :metadata do property :filename, as: :fileName @@ -63,6 +57,14 @@ module API property :digest, render_nil: false end + + property :file, + setter: ->(fragment:, represented:, doc:, **) { + filename = represented.filename || doc.dig('metadata', 'fileName') + self.file = OpenProject::Files.build_uploaded_file fragment[:tempfile], + fragment[:type], + file_name: filename + } end end end From 7f1b777d45798c569719992135af79213072adc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Fri, 2 Jul 2021 21:14:21 +0200 Subject: [PATCH 05/68] Add presence check to filename --- app/contracts/attachments/create_contract.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/contracts/attachments/create_contract.rb b/app/contracts/attachments/create_contract.rb index f7eed10e0b..77e1a074ce 100644 --- a/app/contracts/attachments/create_contract.rb +++ b/app/contracts/attachments/create_contract.rb @@ -40,6 +40,8 @@ module Attachments attribute :container_type attribute :author + validates :filename, presence: true + validate :validate_attachments_addable validate :validate_container_addable validate :validate_author From 2a8bb15d8092f6ccff117a36e0bbe234b458e33f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Fri, 2 Jul 2021 21:24:34 +0200 Subject: [PATCH 06/68] Fix expected validation message --- .../api/v3/attachments/attachment_resource_shared_examples.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb index b0f9b3068d..04d93d92ce 100644 --- a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb +++ b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb @@ -276,7 +276,7 @@ shared_examples 'an APIv3 attachment resource', type: :request, content_type: :j let(:metadata) { Hash.new } it_behaves_like 'constraint violation' do - let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" } + let(:message) { "File #{I18n.t('activerecord.errors.messages.blank')}" } end end From 087360bad09eaf745fba3a0b85e9c2b2bcb57272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Fri, 2 Jul 2021 21:27:40 +0200 Subject: [PATCH 07/68] We no longer raise a multipart error when metadata is empty --- .../api/v3/attachments/attachment_resource_shared_examples.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb index 04d93d92ce..24040b2d9d 100644 --- a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb +++ b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb @@ -255,7 +255,9 @@ shared_examples 'an APIv3 attachment resource', type: :request, content_type: :j context 'metadata section is missing' do let(:request_parts) { { file: file } } - it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') + it_behaves_like 'constraint violation' do + let(:message) { "File #{I18n.t('activerecord.errors.messages.blank')}" } + end end context 'file section is missing' do From b94b0e630d2a7fba9ec12021817a214871689e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Sat, 3 Jul 2021 20:28:17 +0200 Subject: [PATCH 08/68] Fix filesize validation on prepared uploads --- app/models/attachment.rb | 2 +- .../attachments/prepare_upload_service.rb | 40 ++++-------- .../set_prepared_attributes_service.rb | 64 +++++++++++++++++++ .../attachment_resource_shared_examples.rb | 7 +- 4 files changed, 83 insertions(+), 30 deletions(-) create mode 100644 app/services/attachments/set_prepared_attributes_service.rb diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 5d24495e59..89b844f4bc 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -262,7 +262,7 @@ class Attachment < ApplicationRecord private def filesize_below_allowed_maximum - if filesize > Setting.attachment_max_size.to_i.kilobytes + if filesize.to_i > Setting.attachment_max_size.to_i.kilobytes errors.add(:file, :file_too_large, count: Setting.attachment_max_size.to_i.kilobytes) end end diff --git a/app/services/attachments/prepare_upload_service.rb b/app/services/attachments/prepare_upload_service.rb index 0f7a9546d6..abc1a55c5b 100644 --- a/app/services/attachments/prepare_upload_service.rb +++ b/app/services/attachments/prepare_upload_service.rb @@ -26,39 +26,23 @@ # See docs/COPYRIGHT.rdoc for more details. #++ -class Attachments::PrepareUploadService < ::BaseServices::Create - def instance(params) - binding.pry - super(params).tap do |attachment| - # We need to do it like this because `file` is an uploader which expects a File (not a string) - # to upload usually. But in this case the data has already been uploaded and we just point to it. - attachment[:file] = pending_direct_upload_filename(params[:filename]) +module Attachments + class PrepareUploadService < ::BaseServices::Create + def persist(call) + attachment = call.result - attachment.extend(OpenProject::ChangedBySystem) - attachment.change_by_system do - attachment.downloads = -1 - attachment.content_type = params[:content_type] || 'application/octet-stream' + if attachment.save + attachment.reload # necessary so that the fog file uploader path is correct + ServiceResult.new success: true, result: attachment + else + ServiceResult.new success: false, result: attachment end end - end - def persist(call) - attachment = call.result + private - if attachment.save - attachment.reload # necessary so that the fog file uploader path is correct - ServiceResult.new success: true, result: attachment - else - ServiceResult.new success: false, result: attachment + def attributes_service_class + SetPreparedAttributesService end end - - private - - # The name has to be in the same format as what Carrierwave will produce later on. If they are different, - # Carrierwave will alter the name (both local and remote) whenever the attachment is saved with the remote - # file loaded. - def pending_direct_upload_filename(filename) - CarrierWave::SanitizedFile.new(nil).send(:sanitize, filename) - end end diff --git a/app/services/attachments/set_prepared_attributes_service.rb b/app/services/attachments/set_prepared_attributes_service.rb new file mode 100644 index 0000000000..b97f25ac3f --- /dev/null +++ b/app/services/attachments/set_prepared_attributes_service.rb @@ -0,0 +1,64 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Attachments + class SetPreparedAttributesService < SetAttributesService + private + + def set_attributes(params) + super + + set_prepared_attributes params + end + + def set_prepared_attributes(params) + # We need to do it like this because `file` is an uploader which expects a File (not a string) + # to upload usually. But in this case the data has already been uploaded and we just point to it. + model[:file] = pending_direct_upload_filename(params[:filename]) + + # Explicitly set the filesize from metadata + # as the provided file is not actually uploaded + model.filesize = params[:filesize] + + model.extend(OpenProject::ChangedBySystem) + model.change_by_system do + model.downloads = -1 + model.content_type = params[:content_type] || 'application/octet-stream' + end + end + + # The name has to be in the same format as what Carrierwave will produce later on. If they are different, + # Carrierwave will alter the name (both local and remote) whenever the attachment is saved with the remote + # file loaded. + def pending_direct_upload_filename(filename) + CarrierWave::SanitizedFile.new(nil).send(:sanitize, filename) + end + end +end diff --git a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb index 24040b2d9d..6926c64c92 100644 --- a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb +++ b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb @@ -69,10 +69,15 @@ shared_examples 'it supports direct uploads' do context 'with no filesize metadata' do let(:metadata) { { fileName: 'cat.png' } } + let(:json) { JSON.parse subject.body } it 'should respond with 422 due to missing file size metadata' do expect(subject.status).to eq(422) - expect(subject.body).to include 'fileSize' + expect(subject.body).to include 'Size' + end + + it_behaves_like 'constraint violation' do + let(:message) { "Size #{I18n.t('activerecord.errors.messages.blank')}" } end end From f60b86da2d1d7dfa2089fcc20795b5b73046edee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Sat, 3 Jul 2021 20:49:08 +0200 Subject: [PATCH 09/68] Add parser error if invalid metadata json --- app/contracts/attachments/prepare_upload_contract.rb | 2 -- lib/api/v3/attachments/attachments_by_container_api.rb | 2 ++ .../api/v3/attachments/attachment_resource_shared_examples.rb | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/contracts/attachments/prepare_upload_contract.rb b/app/contracts/attachments/prepare_upload_contract.rb index 456c2eb0ab..91f6a717de 100644 --- a/app/contracts/attachments/prepare_upload_contract.rb +++ b/app/contracts/attachments/prepare_upload_contract.rb @@ -31,8 +31,6 @@ module Attachments class PrepareUploadContract < CreateContract validate :validate_direct_uploads_active - # prepared uploads require a filesize to be present - validates :filesize, presence: true private diff --git a/lib/api/v3/attachments/attachments_by_container_api.rb b/lib/api/v3/attachments/attachments_by_container_api.rb index c9b76afede..6616b8e0b0 100644 --- a/lib/api/v3/attachments/attachments_by_container_api.rb +++ b/lib/api/v3/attachments/attachments_by_container_api.rb @@ -58,6 +58,8 @@ module API request.params.tap do |params| params[:metadata] = JSON.parse(params[:metadata]) if params.key?(:metadata) end + rescue JSON::ParserError + raise ::API::Errors::InvalidRequestBody.new(I18n.t('api_v3.errors.multipart_body_error')) end end diff --git a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb index 6926c64c92..0a88b8199f 100644 --- a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb +++ b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb @@ -289,6 +289,7 @@ shared_examples 'an APIv3 attachment resource', type: :request, content_type: :j context 'file is too large' do let(:file) { mock_uploaded_file(content: 'a' * 2.kilobytes) } + let(:expanded_localization) do I18n.t('activerecord.errors.messages.file_too_large', count: max_file_size.kilobytes) end From b265818e3bf782490ecc1e45e42188e07f49e3cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Sat, 3 Jul 2021 21:19:41 +0200 Subject: [PATCH 10/68] When attachment is not saved, use filename property --- app/models/attachment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 89b844f4bc..b70b90ede9 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -148,7 +148,7 @@ class Attachment < ApplicationRecord end def filename - attributes['file'] + attributes['file'] || super end ## From b1351acb00402b90be7f242924fb7956e9f10887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Sat, 3 Jul 2021 21:19:51 +0200 Subject: [PATCH 11/68] Return correct error message on JSON parser erroro --- lib/api/v3/attachments/attachments_by_container_api.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/v3/attachments/attachments_by_container_api.rb b/lib/api/v3/attachments/attachments_by_container_api.rb index 6616b8e0b0..78b93858b2 100644 --- a/lib/api/v3/attachments/attachments_by_container_api.rb +++ b/lib/api/v3/attachments/attachments_by_container_api.rb @@ -59,7 +59,7 @@ module API params[:metadata] = JSON.parse(params[:metadata]) if params.key?(:metadata) end rescue JSON::ParserError - raise ::API::Errors::InvalidRequestBody.new(I18n.t('api_v3.errors.multipart_body_error')) + raise ::API::Errors::InvalidRequestBody.new(I18n.t('api_v3.errors.invalid_json')) end end From 0ed37a6e8a180f94234ad908056c9c56e90e1432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Sat, 3 Jul 2021 21:19:54 +0200 Subject: [PATCH 12/68] Fix specs --- .../attachment_resource_shared_examples.rb | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb index 0a88b8199f..2788b911f8 100644 --- a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb +++ b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb @@ -45,6 +45,7 @@ shared_examples 'it supports direct uploads' do let(:request_parts) { { metadata: metadata.to_json, file: file } } let(:metadata) { { fileName: 'cat.png', fileSize: file.size, contentType: 'image/png' } } let(:file) { mock_uploaded_file(name: 'original-filename.txt') } + let(:json_response) { JSON.parse last_response.body } def request! post request_path, request_parts @@ -57,8 +58,12 @@ shared_examples 'it supports direct uploads' do request! end - it 'should respond with HTTP Not Found' do - expect(subject.status).to eq(404) + it 'should respond with validation error' do + expect(subject.status).to eq(422) + end + + it_behaves_like 'constraint violation' do + let(:message) { "is not available due to a system configuration" } end end @@ -270,7 +275,9 @@ shared_examples 'an APIv3 attachment resource', type: :request, content_type: :j # however as long as we depend on correctly named sections this test should do just fine let(:request_parts) { { metadata: metadata.to_json, wrongFileSection: file } } - it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') + it_behaves_like 'constraint violation' do + let(:message) { "Content type #{I18n.t('activerecord.errors.messages.blank')}" } + end end context 'metadata section is no valid JSON' do @@ -546,7 +553,11 @@ shared_examples 'an APIv3 attachment resource', type: :request, content_type: :j context 'metadata section is missing' do let(:request_parts) { { file: file } } - it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') + it_behaves_like 'constraint violation' do + # File here is the localized name for fileName property + # which is derived from the missing metadata + let(:message) { "File #{I18n.t('activerecord.errors.messages.blank')}" } + end end context 'file section is missing' do @@ -554,7 +565,9 @@ shared_examples 'an APIv3 attachment resource', type: :request, content_type: :j # however as long as we depend on correctly named sections this test should do just fine let(:request_parts) { { metadata: metadata.to_json, wrongFileSection: file } } - it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') + it_behaves_like 'constraint violation' do + let(:message) { "Content type #{I18n.t('activerecord.errors.messages.blank')}" } + end end context 'metadata section is no valid JSON' do @@ -567,7 +580,7 @@ shared_examples 'an APIv3 attachment resource', type: :request, content_type: :j let(:metadata) { Hash.new } it_behaves_like 'constraint violation' do - let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" } + let(:message) { "File #{I18n.t('activerecord.errors.messages.blank')}" } end end From f026330d19ff62efbc97477b11e182fe1fc72209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Sat, 3 Jul 2021 21:30:31 +0200 Subject: [PATCH 13/68] Use attachment upload representer --- lib/api/v3/attachments/attachment_upload_representer.rb | 2 +- lib/api/v3/attachments/attachments_by_container_api.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/api/v3/attachments/attachment_upload_representer.rb b/lib/api/v3/attachments/attachment_upload_representer.rb index 3cbe0742d7..01bdf1e065 100644 --- a/lib/api/v3/attachments/attachment_upload_representer.rb +++ b/lib/api/v3/attachments/attachment_upload_representer.rb @@ -69,7 +69,7 @@ module API attr_reader :form_url, :form_fields, :attachment - def initialize(attachment, current_user:) + def initialize(attachment, current_user:, embed_links: false) super fog_hash = DirectFogUploader.direct_fog_hash attachment: attachment diff --git a/lib/api/v3/attachments/attachments_by_container_api.rb b/lib/api/v3/attachments/attachments_by_container_api.rb index 78b93858b2..69ae7908d9 100644 --- a/lib/api/v3/attachments/attachments_by_container_api.rb +++ b/lib/api/v3/attachments/attachments_by_container_api.rb @@ -94,6 +94,7 @@ module API instance_exec &::API::V3::Utilities::Endpoints::Create .new(model: ::Attachment, parse_representer: AttachmentParsingRepresenter, + render_representer: AttachmentUploadRepresenter, process_service: ::Attachments::PrepareUploadService, process_contract: ::Attachments::PrepareUploadContract, params_getter: method(:parse_multipart), From 450223331206c06740242ff7405eb85a00929411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Sat, 3 Jul 2021 21:54:18 +0200 Subject: [PATCH 14/68] Fix direct uploads mocks with new service layer --- spec/support/shared/with_direct_uploads.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/support/shared/with_direct_uploads.rb b/spec/support/shared/with_direct_uploads.rb index 280e3a99d2..ceee1aa2d4 100644 --- a/spec/support/shared/with_direct_uploads.rb +++ b/spec/support/shared/with_direct_uploads.rb @@ -78,14 +78,13 @@ class WithDirectUploads end def mock_attachment - allow(Attachment).to receive(:create) do |*args| + allow_any_instance_of(::Attachments::PrepareUploadService) + .to receive(:instance) do # We don't use create here because this would cause an infinite loop as FogAttachment's #create # uses the base class's #create which is what we are mocking here. All this is necessary to begin # with because the Attachment class is initialized with the LocalFileUploader before this test # is ever run and we need remote attachments using the FogFileUploader in this scenario. - record = FogAttachment.new *args - record.save - record + FogAttachment.new end # This is so the uploaded callback works. Since we can't actually substitute the Attachment class From badb6050f22ab6c72a132397a761fdda982517c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Sat, 3 Jul 2021 22:03:42 +0200 Subject: [PATCH 15/68] Lint --- lib/api/utilities/endpoints/create.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/utilities/endpoints/create.rb b/lib/api/utilities/endpoints/create.rb index 67714002f1..d5bc386c05 100644 --- a/lib/api/utilities/endpoints/create.rb +++ b/lib/api/utilities/endpoints/create.rb @@ -29,7 +29,7 @@ module API module Utilities module Endpoints - class Create < Modify + class Create < Modify def default_instance_generator(_model) ->(_params) do end From 4c3307f8aa27facbbe861a5d100c88e37bc9d1f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Sun, 4 Jul 2021 20:56:27 +0200 Subject: [PATCH 16/68] Fix export job using attachment service --- .../work_packages/exports/export_job.rb | 23 ++++++++++++------- config/locales/en.yml | 1 + 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/workers/work_packages/exports/export_job.rb b/app/workers/work_packages/exports/export_job.rb index f98f10ef07..c0a4544b05 100644 --- a/app/workers/work_packages/exports/export_job.rb +++ b/app/workers/work_packages/exports/export_job.rb @@ -88,16 +88,23 @@ module WorkPackages end end - def store_attachment(storage, file) - attachment = Attachments::CreateService - .new(storage, author: User.current) - .call(uploaded_file: file, description: '') + def store_attachment(container, file) + call = Attachments::CreateService + .new(user: User.current) + .call(container: container, file: file, filename: File.basename(file), description: '') - download_url = ::API::V3::Utilities::PathHelper::ApiV3Path.attachment_content(attachment.id) + call.on_success do + download_url = ::API::V3::Utilities::PathHelper::ApiV3Path.attachment_content(call.result.id) - upsert_status status: :success, - message: I18n.t('export.succeeded'), - payload: download_payload(download_url) + upsert_status status: :success, + message: I18n.t('export.succeeded'), + payload: download_payload(download_url) + end + + call.on_failure do + upsert_status status: :failure, + message: I18n.t('export.failed', message: call.message) + end end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index d5f1142819..09e42c08dc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1266,6 +1266,7 @@ en: export: your_work_packages_export: "Your work packages export" succeeded: "The export has completed successfully." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" From 6972dac08949e067b0b251844dac66af8e5fc9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Sun, 4 Jul 2021 20:56:39 +0200 Subject: [PATCH 17/68] Fix IFC controller using attachment prepare service --- app/services/base_services/base_contracted.rb | 2 +- .../bim/ifc_models/ifc_models_controller.rb | 22 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/services/base_services/base_contracted.rb b/app/services/base_services/base_contracted.rb index a2d688a2ce..91af341097 100644 --- a/app/services/base_services/base_contracted.rb +++ b/app/services/base_services/base_contracted.rb @@ -29,7 +29,7 @@ #++ module BaseServices - class BaseContracted < BaseCallable +class BaseContracted < BaseCallable include Contracted include Shared::ServiceContext diff --git a/modules/bim/app/controllers/bim/ifc_models/ifc_models_controller.rb b/modules/bim/app/controllers/bim/ifc_models/ifc_models_controller.rb index 554081ac57..e72bd74890 100644 --- a/modules/bim/app/controllers/bim/ifc_models/ifc_models_controller.rb +++ b/modules/bim/app/controllers/bim/ifc_models/ifc_models_controller.rb @@ -176,14 +176,20 @@ module Bim private def prepare_form(ifc_model) - if OpenProject::Configuration.direct_uploads? - @pending_upload = Attachment.create_pending_direct_upload(file_name: "model.ifc", author: current_user) - @form = DirectFogUploader.direct_fog_hash( - attachment: @pending_upload, - success_action_redirect: direct_upload_finished_bcf_project_ifc_models_url - ) - session[:pending_ifc_model_ifc_model_id] = ifc_model.id unless ifc_model.new_record? - end + return unless OpenProject::Configuration.direct_uploads? + + call = ::Attachments::PrepareUploadService + .new(user: current_user) + .call(filename: "model.ifc", filesize: 0) + + call.on_failure { flash[:error] = call.message } + + @pending_upload = call.result + @form = DirectFogUploader.direct_fog_hash( + attachment: @pending_upload, + success_action_redirect: direct_upload_finished_bcf_project_ifc_models_url + ) + session[:pending_ifc_model_ifc_model_id] = ifc_model.id unless ifc_model.new_record? end def frontend_redirect(model_ids) From c16f03407b8f47c3af539f381074205b4abd46ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Sun, 4 Jul 2021 21:09:31 +0200 Subject: [PATCH 18/68] Fix export job --- app/services/base_services/base_contracted.rb | 3 ++- app/workers/backup_job.rb | 27 ++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/app/services/base_services/base_contracted.rb b/app/services/base_services/base_contracted.rb index 91af341097..25dbc8bf12 100644 --- a/app/services/base_services/base_contracted.rb +++ b/app/services/base_services/base_contracted.rb @@ -29,7 +29,7 @@ #++ module BaseServices -class BaseContracted < BaseCallable + class BaseContracted < BaseCallable include Contracted include Shared::ServiceContext @@ -92,6 +92,7 @@ class BaseContracted < BaseCallable # nothing for now but subclasses can override call end + alias_method :after_save, :after_perform def persist(call) diff --git a/app/workers/backup_job.rb b/app/workers/backup_job.rb index b5e76b448b..964d5229ee 100644 --- a/app/workers/backup_job.rb +++ b/app/workers/backup_job.rb @@ -120,17 +120,24 @@ class BackupJob < ::ApplicationJob def store_backup(file_name, backup:, user:) File.open(file_name) do |file| - attachment = Attachments::CreateService - .new(backup, author: user) - .call(uploaded_file: file, description: 'OpenProject backup') - - download_url = ::API::V3::Utilities::PathHelper::ApiV3Path.attachment_content(attachment.id) + call = Attachments::CreateService + .new(user: user) + .call(container: backup, filename: file_name, file: file, description: 'OpenProject backup') + + call.on_success do + download_url = ::API::V3::Utilities::PathHelper::ApiV3Path.attachment_content(call.result.id) + + upsert_status( + status: :success, + message: I18n.t('export.succeeded'), + payload: download_payload(download_url) + ) + end - upsert_status( - status: :success, - message: I18n.t('export.succeeded'), - payload: download_payload(download_url) - ) + call.on_failure do + upsert_status status: :failure, + message: I18n.t('export.failed', message: call.message) + end end end From e24b66164abca597d17de19ba56d431741d7db29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 5 Jul 2021 10:28:50 +0200 Subject: [PATCH 19/68] RenameRename params_getter to params_source --- lib/api/utilities/endpoints/bodied.rb | 10 +++++----- lib/api/v3/attachments/attachments_by_container_api.rb | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/api/utilities/endpoints/bodied.rb b/lib/api/utilities/endpoints/bodied.rb index 39494f0eea..86ed88884e 100644 --- a/lib/api/utilities/endpoints/bodied.rb +++ b/lib/api/utilities/endpoints/bodied.rb @@ -46,7 +46,7 @@ module API end end - def default_params_getter + def default_params_source ->(request) do request.request_body end @@ -56,7 +56,7 @@ module API api_name: model.name.demodulize, instance_generator: default_instance_generator(model), params_modifier: default_params_modifier, - params_getter: default_params_getter, + params_source: default_params_source, process_state: default_process_state, parse_representer: nil, render_representer: nil, @@ -67,7 +67,7 @@ module API self.api_name = api_name self.instance_generator = instance_generator self.params_modifier = params_modifier - self.params_getter = params_getter + self.params_source = params_source self.process_state = process_state self.parse_representer = parse_representer || deduce_parse_representer self.render_representer = render_representer || deduce_render_representer @@ -94,7 +94,7 @@ module API .new(request.current_user, model: model, representer: parse_representer) - .call(params_getter.call(request)) + .call(params_source.call(request)) .result end @@ -129,7 +129,7 @@ module API :instance_generator, :parse_representer, :render_representer, - :params_getter, + :params_source, :params_modifier, :process_contract, :process_service, diff --git a/lib/api/v3/attachments/attachments_by_container_api.rb b/lib/api/v3/attachments/attachments_by_container_api.rb index 69ae7908d9..d2babf01d5 100644 --- a/lib/api/v3/attachments/attachments_by_container_api.rb +++ b/lib/api/v3/attachments/attachments_by_container_api.rb @@ -79,7 +79,7 @@ module API instance_exec &::API::V3::Utilities::Endpoints::Create .new(model: ::Attachment, parse_representer: AttachmentParsingRepresenter, - params_getter: method(:parse_multipart), + params_source: method(:parse_multipart), params_modifier: ->(params) do params.merge(container: container) end) @@ -97,7 +97,7 @@ module API render_representer: AttachmentUploadRepresenter, process_service: ::Attachments::PrepareUploadService, process_contract: ::Attachments::PrepareUploadContract, - params_getter: method(:parse_multipart), + params_source: method(:parse_multipart), params_modifier: ->(params) do params.merge(container: container) end) From 190a75c19239de89344738068e1d1b31fcaad1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 5 Jul 2021 14:30:04 +0200 Subject: [PATCH 20/68] Fix mail handler using attachment service --- app/models/mail_handler.rb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/models/mail_handler.rb b/app/models/mail_handler.rb index 471d6dca74..5beaddf97d 100644 --- a/app/models/mail_handler.rb +++ b/app/models/mail_handler.rb @@ -272,9 +272,15 @@ class MailHandler < ActionMailer::Base binary: true ) - ::Attachments::CreateService - .new(container, author: user) - .call(uploaded_file: file, description: nil) + call = ::Attachments::CreateService + .new(user: user) + .call(container: container, filename: attachment.filename, file: file) + + call.on_failure do + log "Failed to add attachment #{attachment.filename} for [#{sender_email}]: #{call.message}" + end + + call.result end # Adds To and Cc as watchers of the given object if the sender has the From 50025487bff8a3dbe56a4575af04ba136a08c057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 5 Jul 2021 16:17:03 +0200 Subject: [PATCH 21/68] Fix usage of attachment create service in documents --- modules/documents/spec/models/document_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/documents/spec/models/document_spec.rb b/modules/documents/spec/models/document_spec.rb index b76ae23234..12813cb52e 100644 --- a/modules/documents/spec/models/document_spec.rb +++ b/modules/documents/spec/models/document_spec.rb @@ -86,8 +86,8 @@ describe Document do expect do Attachments::CreateService - .new(valid_document, author: admin) - .call(uploaded_file: FactoryBot.attributes_for(:attachment)[:file], description: '') + .new(user: admin) + .call(container: valid_document, file: FactoryBot.attributes_for(:attachment)[:file], filename: 'foo') expect(valid_document.attachments.size).to eql 1 end.to(change do From d30680e40fbf7a6bb0b665c05947062dd80c20da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 5 Jul 2021 16:21:51 +0200 Subject: [PATCH 22/68] Reuse shared examples for document attachment spec --- .../attachments_by_documents_resource_spec.rb | 114 ++---------------- 1 file changed, 10 insertions(+), 104 deletions(-) diff --git a/modules/documents/spec/requests/api/v3/attachments/attachments_by_documents_resource_spec.rb b/modules/documents/spec/requests/api/v3/attachments/attachments_by_documents_resource_spec.rb index 4c12f43f30..552aef077b 100644 --- a/modules/documents/spec/requests/api/v3/attachments/attachments_by_documents_resource_spec.rb +++ b/modules/documents/spec/requests/api/v3/attachments/attachments_by_documents_resource_spec.rb @@ -27,113 +27,19 @@ #++ require 'spec_helper' -require 'rack/test' +require 'requests/api/v3/attachments/attachment_resource_shared_examples' -describe 'API v3 Attachments by document resource', type: :request do - include Rack::Test::Methods - include API::V3::Utilities::PathHelper - include FileHelpers +describe "document attachments" do + it_behaves_like "an APIv3 attachment resource" do + let(:attachment_type) { :document } - let(:current_user) do - FactoryBot.create(:user, - member_in_project: project, - member_through_role: role) - end - let(:project) { FactoryBot.create(:project) } - let(:role) { FactoryBot.create(:role, permissions: permissions) } - let(:permissions) { [:view_documents] } - let(:document) { FactoryBot.create(:document, project: project) } - - subject(:response) { last_response } - - before do - allow(User).to receive(:current).and_return current_user - end - - describe '#get' do - let(:get_path) { api_v3_paths.attachments_by_document document.id } - - before do - FactoryBot.create_list(:attachment, 2, container: document) - get get_path - end - - it 'should respond with 200' do - expect(subject.status).to eq(200) - end - - it_behaves_like 'API V3 collection response', 2, 2, 'Attachment' - end - - describe '#post' do - let(:permissions) { %i[view_documents manage_documents] } - - let(:request_path) { api_v3_paths.attachments_by_document document.id } - let(:request_parts) { { metadata: metadata.to_json, file: file } } - let(:metadata) { { fileName: 'cat.png' } } - let(:file) { mock_uploaded_file(name: 'original-filename.txt') } - let(:max_file_size) { 1 } # given in kiB - - before do - allow(Setting).to receive(:attachment_max_size).and_return max_file_size.to_s - post request_path, request_parts - end - - it 'should respond with HTTP Created' do - expect(subject.status).to eq(201) - end - - it 'should return the new attachment' do - expect(subject.body).to be_json_eql('Attachment'.to_json).at_path('_type') - end + let(:create_permission) { :manage_documents } + let(:read_permission) { :view_documents } + let(:update_permission) { :manage_documents } - it 'ignores the original file name' do - expect(subject.body).to be_json_eql('cat.png'.to_json).at_path('fileName') - end - - context 'metadata section is missing' do - let(:request_parts) { { file: file } } - - it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') - end - - context 'file section is missing' do - # rack-test won't send a multipart request without a file being present - # however as long as we depend on correctly named sections this test should do just fine - let(:request_parts) { { metadata: metadata.to_json, wrongFileSection: file } } - - it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') - end - - context 'metadata section is no valid JSON' do - let(:request_parts) { { metadata: '"fileName": "cat.png"', file: file } } - - it_behaves_like 'parse error' - end - - context 'metadata is missing the fileName' do - let(:metadata) { Hash.new } - - it_behaves_like 'constraint violation' do - let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" } - end - end - - context 'file is too large' do - let(:file) { mock_uploaded_file(content: 'a' * 2.kilobytes) } - let(:expanded_localization) do - I18n.t('activerecord.errors.messages.file_too_large', count: max_file_size.kilobytes) - end - - it_behaves_like 'constraint violation' do - let(:message) { "File #{expanded_localization}" } - end - end - - context 'only allowed to view documents' do - let(:permissions) { [:view_documents] } - - it_behaves_like 'unauthorized access' + let(:document) do + FactoryBot.create :document, project: project end end end + From 0c437c3f052bfa3b14981fc1bb0f504299ba1edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 5 Jul 2021 16:24:50 +0200 Subject: [PATCH 23/68] Fix stubbed attachment service in export job spec --- spec/workers/work_packages/exports/export_job_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/workers/work_packages/exports/export_job_spec.rb b/spec/workers/work_packages/exports/export_job_spec.rb index e9ae9a9a51..99d2d92f86 100644 --- a/spec/workers/work_packages/exports/export_job_spec.rb +++ b/spec/workers/work_packages/exports/export_job_spec.rb @@ -69,21 +69,21 @@ describe WorkPackages::Exports::ExportJob do expect(Attachments::CreateService) .to receive(:new) - .with(export, author: user) + .with(user: user) .and_return(service) expect(WorkPackages::Exports::CleanupOutdatedJob) .to receive(:perform_after_grace) expect(service) - .to receive(:call) do |uploaded_file:, description:| - expect(File.basename(uploaded_file)) + .to receive(:call) do |file:, **args| + expect(File.basename(file)) .to start_with 'some_title' - expect(File.basename(uploaded_file)) + expect(File.basename(file)) .to end_with ".#{mime_type}" - attachment + ServiceResult.new(result: attachment, success: true) end allow("WorkPackage::Exporter::#{mime_type.upcase}".constantize) From d30352ccaad9a0aa845b57094a980f5b09583aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 5 Jul 2021 16:26:32 +0200 Subject: [PATCH 24/68] Use admin user in backup spec --- spec/workers/backup_job_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/workers/backup_job_spec.rb b/spec/workers/backup_job_spec.rb index 03e1c86713..3af5c10139 100644 --- a/spec/workers/backup_job_spec.rb +++ b/spec/workers/backup_job_spec.rb @@ -59,7 +59,7 @@ describe BackupJob, type: :model do let(:arguments) { [{ backup: backup, user: user, **opts }] } - let(:user) { FactoryBot.create :user } + let(:user) { FactoryBot.create :admin } before do previous_backup; backup; status # create From 6ff21c6b961d8cc19f7e52ae834652237d3db1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 5 Jul 2021 20:00:53 +0200 Subject: [PATCH 25/68] Fix export job for bim --- .../work_packages/exports/export_job_spec.rb | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/modules/bim/spec/workers/work_packages/exports/export_job_spec.rb b/modules/bim/spec/workers/work_packages/exports/export_job_spec.rb index de62b36fb2..2e4b87d2f7 100644 --- a/modules/bim/spec/workers/work_packages/exports/export_job_spec.rb +++ b/modules/bim/spec/workers/work_packages/exports/export_job_spec.rb @@ -59,22 +59,16 @@ describe WorkPackages::Exports::ExportJob do let(:mime_type) { :bcf } it 'issues an OpenProject::Bim::BcfXml::Exporter export' do - file = double(File) - allow(file) - .to receive(:is_a?) - .with(File) - .and_return true - result = WorkPackage::Exporter::Result::Success.new(format: 'blubs', - title: 'some_title', - content: file, + title: "some_title.#{mime_type}", + content: 'some content', mime_type: "application/octet-stream") service = double('attachments create service') expect(Attachments::CreateService) .to receive(:new) - .with(export, author: user) + .with(user: user) .and_return(service) expect(WorkPackages::Exports::CleanupOutdatedJob) @@ -82,7 +76,7 @@ describe WorkPackages::Exports::ExportJob do expect(service) .to(receive(:call)) - .and_return attachment + .and_return(ServiceResult.new(result: attachment, success: true)) allow(OpenProject::Bim::BcfXml::Exporter) .to receive(:list) From 9e26691db75d72282006a91f10ebce6ade3e27da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 5 Jul 2021 20:09:51 +0200 Subject: [PATCH 26/68] Fix attachment integration spec --- .../create_service_integration_spec.rb | 51 ++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/spec/services/attachments/create_service_integration_spec.rb b/spec/services/attachments/create_service_integration_spec.rb index 135f23d3a7..ec8c03291f 100644 --- a/spec/services/attachments/create_service_integration_spec.rb +++ b/spec/services/attachments/create_service_integration_spec.rb @@ -29,20 +29,26 @@ require 'spec_helper' -describe Attachments::CreateService, 'integration' do - let(:user) { FactoryBot.create(:user) } +describe Attachments::CreateService, 'integration', with_settings: { journal_aggregation_time_minutes: 0 } do let(:description) { 'a fancy description' } - subject { described_class.new(container, author: user) } + subject { described_class.new(user: user) } describe '#call' do def call_tested_method - subject.call uploaded_file: FileHelpers.mock_uploaded_file(name: 'foobar.txt'), + subject.call container: container, + file: FileHelpers.mock_uploaded_file(name: 'foobar.txt'), + filename: 'foobar.txt', description: description end context 'when journalized' do - let(:container) { FactoryBot.create(:work_package) } + shared_let(:container) { FactoryBot.create(:work_package) } + shared_let(:user) do + FactoryBot.create :user, + member_in_project: container.project, + member_with_permissions: %i[view_work_packages edit_work_packages] + end shared_examples 'successful creation' do it 'saves the attachment' do @@ -90,26 +96,22 @@ describe Attachments::CreateService, 'integration' do end context 'with an invalid attachment', with_settings: { attachment_max_size: 0 } do - it 'raises the exception' do + it 'does not raise exceptions' do expect { call_tested_method } - .to raise_exception ActiveRecord::RecordInvalid - end - - it 'does not create the attachment' do - begin - call_tested_method - rescue ActiveRecord::RecordInvalid - # expected - end + .not_to raise_exception ActiveRecord::RecordInvalid - expect(Attachment.count) - .to eq 0 + expect(call_tested_method.errors[:file]).to include "is too large (maximum size is 0 Bytes)." end end end context 'when not journalized' do - let(:container) { FactoryBot.create(:message) } + shared_let(:container) { FactoryBot.create(:message) } + shared_let(:user) do + FactoryBot.create :user, + member_in_project: container.forum.project, + member_with_permissions: %i[add_messages edit_messages] + end shared_examples 'successful creation' do it 'saves the attachment' do @@ -158,6 +160,7 @@ describe Attachments::CreateService, 'integration' do context "when uncontainered" do let(:container) { nil } + let(:user) { FactoryBot.create :admin } before do call_tested_method @@ -169,5 +172,17 @@ describe Attachments::CreateService, 'integration' do expect(attachment.description).to eq description end end + + context "when user with no permissions" do + let(:container) { nil } + let(:user) { FactoryBot.build_stubbed :user } + + it 'does not save an attachment' do + expect do + expect(call_tested_method).to be_failure + expect(call_tested_method.errors[:base]).to include 'may not be accessed.' + end.not_to change { Attachment.count } + end + end end end From a8e38058514909d9f424aba75afb977194eec540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 5 Jul 2021 20:13:36 +0200 Subject: [PATCH 27/68] Fix issues_controller spec --- .../app/controllers/bim/bcf/issues_controller.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/modules/bim/app/controllers/bim/bcf/issues_controller.rb b/modules/bim/app/controllers/bim/bcf/issues_controller.rb index d8bedcce89..6c5a33097e 100644 --- a/modules/bim/app/controllers/bim/bcf/issues_controller.rb +++ b/modules/bim/app/controllers/bim/bcf/issues_controller.rb @@ -213,9 +213,16 @@ module Bim end def create_attachment - Attachments::CreateService - .new(nil, author: current_user) - .call(uploaded_file: params[:bcf_file], description: params[:bcf_file].original_filename) + filename = params[:bcf_file].original_filename + call = Attachments::CreateService + .new(user: current_user) + .call(file: params[:bcf_file], + filename: filename, + description: filename) + + call.on_failure { raise e.message } + + call.result end def check_file_param From 4a57d196010e8c416c77140045c685d7202bdd10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 5 Jul 2021 20:16:22 +0200 Subject: [PATCH 28/68] Make budget resource spec reuse common examples --- .../attachments_by_budget_resource_spec.rb | 119 ++---------------- 1 file changed, 10 insertions(+), 109 deletions(-) diff --git a/modules/costs/spec/requests/api/attachments/attachments_by_budget_resource_spec.rb b/modules/costs/spec/requests/api/attachments/attachments_by_budget_resource_spec.rb index dd5f1e91ed..f5b6a580a1 100644 --- a/modules/costs/spec/requests/api/attachments/attachments_by_budget_resource_spec.rb +++ b/modules/costs/spec/requests/api/attachments/attachments_by_budget_resource_spec.rb @@ -27,118 +27,19 @@ #++ require 'spec_helper' -require 'rack/test' +require 'requests/api/v3/attachments/attachment_resource_shared_examples' -describe 'API v3 Attachments by budget resource', type: :request do - include Rack::Test::Methods - include API::V3::Utilities::PathHelper - include FileHelpers +describe "budget attachments" do + it_behaves_like "an APIv3 attachment resource" do + let(:attachment_type) { :budget } - let(:current_user) do - FactoryBot.create(:user, - member_in_project: project, - member_with_permissions: permissions) - end - let(:project) { FactoryBot.create(:project) } - let(:permissions) { [:view_budgets] } - let(:budget) { FactoryBot.create(:budget, project: project) } - - subject(:response) { last_response } - - before do - allow(User).to receive(:current).and_return current_user - end - - describe '#get' do - let(:get_path) { api_v3_paths.attachments_by_budget budget.id } + let(:create_permission) { :edit_budgets } + let(:read_permission) { :view_budgets } + let(:update_permission) { :edit_budgets } - before do - FactoryBot.create_list(:attachment, 2, container: budget) - get get_path - end - - it 'should respond with 200' do - expect(subject.status).to eq(200) - end - - it_behaves_like 'API V3 collection response', 2, 2, 'Attachment' - end - - describe '#post' do - let(:permissions) { %i[view_budgets edit_budgets] } - - let(:request_path) { api_v3_paths.attachments_by_budget budget.id } - let(:request_parts) { { metadata: metadata.to_json, file: file } } - let(:metadata) { { fileName: 'cat.png' } } - let(:file) { mock_uploaded_file(name: 'original-filename.txt') } - let(:max_file_size) { 1 } # given in kiB - - before do - allow(Setting).to receive(:attachment_max_size).and_return max_file_size.to_s - post request_path, request_parts - end - - it 'should respond with HTTP Created' do - expect(subject.status).to eq(201) - end - - it 'should return the new attachment' do - expect(subject.body).to be_json_eql('Attachment'.to_json).at_path('_type') - end - - it 'ignores the original file name' do - expect(subject.body).to be_json_eql('cat.png'.to_json).at_path('fileName') - end - - context 'metadata section is missing' do - let(:request_parts) { { file: file } } - - it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') - end - - context 'file section is missing' do - # rack-test won't send a multipart request without a file being present - # however as long as we depend on correctly named sections this test should do just fine - let(:request_parts) { { metadata: metadata.to_json, wrongFileSection: file } } - - it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') - end - - context 'metadata section is no valid JSON' do - let(:request_parts) { { metadata: '"fileName": "cat.png"', file: file } } - - it_behaves_like 'parse error' - end - - context 'metadata is missing the fileName' do - let(:metadata) { Hash.new } - - it_behaves_like 'constraint violation' do - let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" } - end - end - - context 'file is too large' do - let(:file) { mock_uploaded_file(content: 'a' * 2.kilobytes) } - let(:expanded_localization) do - I18n.t('activerecord.errors.messages.file_too_large', count: max_file_size.kilobytes) - end - - it_behaves_like 'constraint violation' do - let(:message) { "File #{expanded_localization}" } - end - end - - context 'only allowed to add messages, but no edit permission' do - let(:permissions) { %i[view_messages add_messages] } - - it_behaves_like 'unauthorized access' - end - - context 'only allowed to view messages' do - let(:permissions) { [:view_messages] } - - it_behaves_like 'unauthorized access' + let(:budget) do + FactoryBot.create :budget, project: project end end end + From 92954efc92e53705b93e6b25c4088ad4dac11b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 6 Jul 2021 12:19:32 +0200 Subject: [PATCH 29/68] Fix attachment parsing representer spec --- .../attachment_metadata_representer_spec.rb | 43 ++++++------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/spec/lib/api/v3/attachments/attachment_metadata_representer_spec.rb b/spec/lib/api/v3/attachments/attachment_metadata_representer_spec.rb index 1ea8f18f73..094cdfb84f 100644 --- a/spec/lib/api/v3/attachments/attachment_metadata_representer_spec.rb +++ b/spec/lib/api/v3/attachments/attachment_metadata_representer_spec.rb @@ -28,15 +28,16 @@ require 'spec_helper' -describe ::API::V3::Attachments::AttachmentMetadataRepresenter do +describe ::API::V3::Attachments::AttachmentParsingRepresenter do + let(:current_user) { FactoryBot.build_stubbed :user } include API::V3::Utilities::PathHelper let(:metadata) do data = Hashie::Mash.new - data.file_name = original_file_name + data.filename = original_file_name data.description = original_description data.content_type = original_content_type - data.file_size = original_file_size + data.filesize = original_file_size data.digest = original_digest data end @@ -47,34 +48,18 @@ describe ::API::V3::Attachments::AttachmentMetadataRepresenter do let(:original_file_size) { 42 } let(:original_digest) { "0xFF" } - let(:representer) { ::API::V3::Attachments::AttachmentMetadataRepresenter.new(metadata) } - - describe 'generation' do - subject { representer.to_json } - - it 'is a type-less representer' do - is_expected.not_to have_json_path('_type') - end - - it { is_expected.to be_json_eql(original_file_name.to_json).at_path('fileName') } - it { is_expected.to be_json_eql(original_content_type.to_json).at_path('contentType') } - it { is_expected.to be_json_eql(original_file_size.to_json).at_path('fileSize') } - it { is_expected.to be_json_eql(original_digest.to_json).at_path('digest') } - - it_behaves_like 'API V3 formattable', 'description' do - let(:format) { 'plain' } - let(:raw) { original_description } - end - end + let(:representer) { described_class.new(metadata, current_user: current_user) } describe 'parsing' do let(:parsed_hash) do { - 'fileName' => 'the parsed name', - 'description' => { 'raw' => 'the parsed description' }, - 'contentType' => 'text/html', - 'fileSize' => 43, - 'digest' => '0x00' + 'metadata' => { + 'fileName' => 'the parsed name', + 'description' => { 'raw' => 'the parsed description' }, + 'contentType' => 'text/html', + 'fileSize' => 43, + 'digest' => '0x00' + } } end @@ -84,10 +69,10 @@ describe ::API::V3::Attachments::AttachmentMetadataRepresenter do representer.from_hash parsed_hash end - it { expect(subject.file_name).to eql('the parsed name') } + it { expect(subject.filename).to eql('the parsed name') } it { expect(subject.description).to eql('the parsed description') } it { expect(subject.content_type).to eql('text/html') } - it { expect(subject.file_size).to eql(43) } + it { expect(subject.filesize).to eql(43) } it { expect(subject.digest).to eql('0x00') } end end From bfb304d9b7c015edd8e8518f96a573f0adc21621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 6 Jul 2021 15:50:51 +0200 Subject: [PATCH 30/68] Replace prepare part of attachment spec into separate service spec --- spec/models/attachment_spec.rb | 58 ----------- ...prepare_upload_service_integration_spec.rb | 98 +++++++++++++++++++ 2 files changed, 98 insertions(+), 58 deletions(-) create mode 100644 spec/services/attachments/prepare_upload_service_integration_spec.rb diff --git a/spec/models/attachment_spec.rb b/spec/models/attachment_spec.rb index 3a8b70d157..0235a1c6c2 100644 --- a/spec/models/attachment_spec.rb +++ b/spec/models/attachment_spec.rb @@ -183,64 +183,6 @@ describe Attachment, type: :model do end end - describe '.create_pending_direct_upload' do - let(:file_size) { 6 } - let(:file_name) { 'document.png' } - let(:content_type) { "application/octet-stream" } - - subject do - described_class.create_pending_direct_upload(file_name: file_name, - author: author, - container: container, - content_type: content_type, - file_size: file_size) - end - - it 'returns the attachment' do - expect(subject) - .to be_a(Attachment) - end - - it 'sets the content_type' do - expect(subject.content_type) - .to eql content_type - end - - it 'sets the file_size' do - expect(subject.filesize) - .to eql file_size - end - - it 'sets the file for carrierwave' do - expect(subject.file.file.path) - .to end_with "attachment/file/#{subject.id}/#{file_name}" - end - - it 'sets the author' do - expect(subject.author) - .to eql author - end - - it 'sets the digest to empty string' do - expect(subject.digest) - .to eql "" - end - - it 'sets the download count to -1' do - expect(subject.downloads) - .to eql -1 - end - - context 'with a special character in the filename' do - let(:file_name) { "document=number 5.png" } - - it 'sets the file for carrierwave' do - expect(subject.file.file.path) - .to end_with "attachment/file/#{subject.id}/document_number_5.png" - end - end - end - ## # The tests assumes the default, file-based storage is configured and tests against that. # I.e. it does not test fog attachments being deleted from the cloud storage (such as S3). diff --git a/spec/services/attachments/prepare_upload_service_integration_spec.rb b/spec/services/attachments/prepare_upload_service_integration_spec.rb new file mode 100644 index 0000000000..8db117518f --- /dev/null +++ b/spec/services/attachments/prepare_upload_service_integration_spec.rb @@ -0,0 +1,98 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2020 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 docs/COPYRIGHT.rdoc for more details. + +require 'spec_helper' + +describe Attachments::PrepareUploadService, + 'integration' do + shared_let(:container) { FactoryBot.create(:work_package) } + shared_let(:user) do + FactoryBot.create :user, + member_in_project: container.project, + member_with_permissions: %i[view_work_packages edit_work_packages] + end + let(:instance) { described_class.new(user: user) } + + let(:file_size) { 6 } + let(:file_name) { 'document.png' } + let(:content_type) { "application/octet-stream" } + + let(:call) do + instance.call filename: file_name, + container: container, + content_type: content_type, + filesize: file_size + end + + let(:attachment) { call.result } + + it 'returns the attachment' do + expect(attachment) + .to be_a(Attachment) + end + + it 'sets the content_type' do + expect(attachment.content_type) + .to eql content_type + end + + it 'sets the file_size' do + expect(attachment.filesize) + .to eql file_size + end + + it 'sets the file for carrierwave' do + expect(attachment.file.file.path) + .to end_with "attachment/file/#{attachment.id}/#{file_name}" + end + + it 'sets the author' do + expect(attachment.author) + .to eql user + end + + it 'sets the digest to empty string' do + expect(attachment.digest) + .to eql "" + end + + it 'sets the download count to -1' do + expect(attachment.downloads) + .to eql -1 + end + + context 'with a special character in the filename' do + let(:file_name) { "document=number 5.png" } + + it 'sets the file for carrierwave' do + expect(attachment.file.file.path) + .to end_with "attachment/file/#{attachment.id}/document_number_5.png" + end + end +end From b02776046541047315cec1b6eaa8b1611fd66a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 6 Jul 2021 15:54:06 +0200 Subject: [PATCH 31/68] Clear cache for login spec --- spec/features/auth/login_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/auth/login_spec.rb b/spec/features/auth/login_spec.rb index f3affef1d0..c1d3c8d170 100644 --- a/spec/features/auth/login_spec.rb +++ b/spec/features/auth/login_spec.rb @@ -28,7 +28,7 @@ require 'spec_helper' -describe 'Login', type: :feature do +describe 'Login', type: :feature, clear_cache: true do before do @capybara_ignore_elements = Capybara.ignore_hidden_elements Capybara.ignore_hidden_elements = true From 0fce502a69b2a97ac0cdcbb82723d93ada5a985c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 6 Jul 2021 21:05:18 +0200 Subject: [PATCH 32/68] Convert document create/update into services --- .../concerns/attachable_service_call.rb | 66 +++++++++++++++++++ .../app/contracts/documents/base_contract.rb | 54 +++++++++++++++ .../contracts/documents/create_contract.rb | 34 ++++++++++ .../contracts/documents/update_contract.rb | 34 ++++++++++ .../app/controllers/documents_controller.rb | 46 ++++++++----- .../app/services/documents/create_service.rb | 35 ++++++++++ .../documents/set_attributes_service.rb | 35 ++++++++++ .../app/services/documents/update_service.rb | 35 ++++++++++ .../controllers/documents_controller_spec.rb | 11 ++-- .../services/documents/create_service_spec.rb | 6 ++ .../services/documents/update_service_spec.rb | 36 ++++++++++ 11 files changed, 372 insertions(+), 20 deletions(-) create mode 100644 app/controllers/concerns/attachable_service_call.rb create mode 100644 modules/documents/app/contracts/documents/base_contract.rb create mode 100644 modules/documents/app/contracts/documents/create_contract.rb create mode 100644 modules/documents/app/contracts/documents/update_contract.rb create mode 100644 modules/documents/app/services/documents/create_service.rb create mode 100644 modules/documents/app/services/documents/set_attributes_service.rb create mode 100644 modules/documents/app/services/documents/update_service.rb create mode 100644 modules/job_status/spec/services/documents/create_service_spec.rb create mode 100644 modules/job_status/spec/services/documents/update_service_spec.rb diff --git a/app/controllers/concerns/attachable_service_call.rb b/app/controllers/concerns/attachable_service_call.rb new file mode 100644 index 0000000000..5803a95b19 --- /dev/null +++ b/app/controllers/concerns/attachable_service_call.rb @@ -0,0 +1,66 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module AttachableServiceCall + ## + # Call the presented CreateContract service + # with the given params, merging in any attachment params + # + # @param service_cls the service class instance + # @param args permitted args for the service call + def attachable_create_call(service_cls, args:) + service_cls + .new(user: current_user) + .call(args.merge(attachment_params)) + end + + ## + # Call the presented UpdateContract service + # with the given params, merging in any attachment params + # + # @param service_cls the service class instance + # @param args permitted args for the service call + def attachable_update_call(service_cls, model:, args:) + service_cls + .new(user: current_user, model: model) + .call(args.merge(attachment_params)) + end + + ## + # Attachable parameters mapped to a format the + # SetReplacements service concern + def attachment_params + attachment_params = permitted_params.attachments.to_h + + if attachment_params.any? + { attachment_ids: attachment_params.values.map(&:values).flatten } + else + {} + end + end +end diff --git a/modules/documents/app/contracts/documents/base_contract.rb b/modules/documents/app/contracts/documents/base_contract.rb new file mode 100644 index 0000000000..0670ef94a0 --- /dev/null +++ b/modules/documents/app/contracts/documents/base_contract.rb @@ -0,0 +1,54 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Documents + class BaseContract < ::ModelContract + include Attachments::ValidateReplacements + + def self.model + Document + end + + attribute :project + attribute :category + attribute :title + attribute :description + + validate :validate_manage_allowed + + private + + def validate_manage_allowed + unless user.allowed_to?(:manage_documents, model.project) + errors.add :base, :error_unauthorized + end + end + end +end diff --git a/modules/documents/app/contracts/documents/create_contract.rb b/modules/documents/app/contracts/documents/create_contract.rb new file mode 100644 index 0000000000..29af7bcec6 --- /dev/null +++ b/modules/documents/app/contracts/documents/create_contract.rb @@ -0,0 +1,34 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Documents + class CreateContract < BaseContract + end +end diff --git a/modules/documents/app/contracts/documents/update_contract.rb b/modules/documents/app/contracts/documents/update_contract.rb new file mode 100644 index 0000000000..1c83f98988 --- /dev/null +++ b/modules/documents/app/contracts/documents/update_contract.rb @@ -0,0 +1,34 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Documents + class UpdateContract < BaseContract + end +end diff --git a/modules/documents/app/controllers/documents_controller.rb b/modules/documents/app/controllers/documents_controller.rb index f805cc106e..86b58661f7 100644 --- a/modules/documents/app/controllers/documents_controller.rb +++ b/modules/documents/app/controllers/documents_controller.rb @@ -29,6 +29,7 @@ #++ class DocumentsController < ApplicationController + include AttachableServiceCall default_search_scope :documents model_object Document before_action :find_project_by_project_id, only: %i[index new create] @@ -64,14 +65,14 @@ class DocumentsController < ApplicationController end def create - @document = @project.documents.build - @document.attributes = document_params - @document.attach_files(permitted_params.attachments.to_h) + call = attachable_create_call ::Documents::CreateService, + args: document_params.merge(project: @project) - if @document.save + if call.success? flash[:notice] = I18n.t(:notice_successful_create) redirect_to project_documents_path(@project) else + @document = call.result render action: 'new' end end @@ -81,13 +82,15 @@ class DocumentsController < ApplicationController end def update - @document.attributes = document_params - @document.attach_files(permitted_params.attachments.to_h) + call = attachable_update_call ::Documents::UpdateService, + model: @document, + args: document_params - if @document.save + if call.success? flash[:notice] = I18n.t(:notice_successful_update) redirect_to action: 'show', id: @document else + @document = call.result render action: 'edit' end end @@ -98,18 +101,19 @@ class DocumentsController < ApplicationController end def add_attachment - @document.attach_files(permitted_params.attachments.to_h) - attachments = @document.attachments.select(&:new_record?) + current_attachments = @document.attachments.pluck(:id) + call = attachable_update_call ::Documents::UpdateService, + model: @document, + args: document_params - @document.save - saved_attachments = attachments.select(&:persisted?) + if call.success? && Setting.notified_events.include?('document_added') + added = call.result + .attachments + .reject { |a| current_attachments.include?(a.id) } - if saved_attachments.present? && Setting.notified_events.include?('document_added') - users = saved_attachments.first.container.recipients - users.each do |user| - UserMailer.attachments_added(user, saved_attachments).deliver_later - end + notify_attachments added end + redirect_to action: 'show', id: @document end @@ -118,4 +122,14 @@ class DocumentsController < ApplicationController def document_params params.fetch(:document, {}).permit('category_id', 'title', 'description') end + + def notify_attachments(added) + return if added.empty? + + users = added.first.container.recipients + users.each do |user| + UserMailer.attachments_added(user, added).deliver_later + end + end + end diff --git a/modules/documents/app/services/documents/create_service.rb b/modules/documents/app/services/documents/create_service.rb new file mode 100644 index 0000000000..62ba3665e6 --- /dev/null +++ b/modules/documents/app/services/documents/create_service.rb @@ -0,0 +1,35 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Documents + class CreateService < ::BaseServices::Create + include Attachments::ReplaceAttachments + end +end diff --git a/modules/documents/app/services/documents/set_attributes_service.rb b/modules/documents/app/services/documents/set_attributes_service.rb new file mode 100644 index 0000000000..c9d4a807f3 --- /dev/null +++ b/modules/documents/app/services/documents/set_attributes_service.rb @@ -0,0 +1,35 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Documents + class SetAttributesService < ::BaseServices::SetAttributes + include Attachments::SetReplacements + end +end diff --git a/modules/documents/app/services/documents/update_service.rb b/modules/documents/app/services/documents/update_service.rb new file mode 100644 index 0000000000..5e7013f28e --- /dev/null +++ b/modules/documents/app/services/documents/update_service.rb @@ -0,0 +1,35 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Documents + class UpdateService < ::BaseServices::Update + include Attachments::ReplaceAttachments + end +end diff --git a/modules/documents/spec/controllers/documents_controller_spec.rb b/modules/documents/spec/controllers/documents_controller_spec.rb index 280bca4453..20f1e318ce 100644 --- a/modules/documents/spec/controllers/documents_controller_spec.rb +++ b/modules/documents/spec/controllers/documents_controller_spec.rb @@ -130,6 +130,7 @@ describe DocumentsController do end describe "with attachments" do + let(:uncontainered) { FactoryBot.create :attachment, container: nil, author: admin } before do notify_project = project FactoryBot.create(:member, project: notify_project, user: user, roles: [role]) @@ -140,7 +141,7 @@ describe DocumentsController do document: FactoryBot.attributes_for(:document, title: "New Document", project_id: notify_project.id, category_id: default_category.id), - attachments: { '1' => { description: "sample file", file: file_attachment } } + attachments: { '1' => { id: uncontainered.id } } } end @@ -149,8 +150,7 @@ describe DocumentsController do expect(document.attachments.count).to eql 1 attachment = document.attachments.first - expect(attachment.description).to eql "sample file" - expect(attachment.filename).to eql "testfile.txt" + expect(uncontainered.reload).to eql attachment end it "should redirect to the documents-page" do @@ -176,12 +176,14 @@ describe DocumentsController do end describe '#add_attachment' do + let(:uncontainered) { FactoryBot.create :attachment, container: nil, author: admin } + before do document post :add_attachment, params: { id: document.id, - attachments: { '1' => { description: "sample file", file: file_attachment } } + attachments: { '1' => { id: uncontainered.id } } } end @@ -189,6 +191,7 @@ describe DocumentsController do expect(response).to be_redirect document.reload expect(document.attachments.length).to eq(1) + expect(uncontainered.reload).to eq document.attachments.first end end diff --git a/modules/job_status/spec/services/documents/create_service_spec.rb b/modules/job_status/spec/services/documents/create_service_spec.rb new file mode 100644 index 0000000000..7bf9c46ff4 --- /dev/null +++ b/modules/job_status/spec/services/documents/create_service_spec.rb @@ -0,0 +1,6 @@ +require 'spec_helper' +require 'services/base_services/behaves_like_create_service' + +describe Documents::CreateService, type: :model do + it_behaves_like 'BaseServices create service' +end \ No newline at end of file diff --git a/modules/job_status/spec/services/documents/update_service_spec.rb b/modules/job_status/spec/services/documents/update_service_spec.rb new file mode 100644 index 0000000000..511f0b398d --- /dev/null +++ b/modules/job_status/spec/services/documents/update_service_spec.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require 'services/base_services/behaves_like_update_service' + +describe Documents::UpdateService, type: :model do + it_behaves_like 'BaseServices update service' +end From 197afad075a8836fc966ea613ad137486ca66664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 7 Jul 2021 09:58:19 +0200 Subject: [PATCH 33/68] Budget services --- .../app/contracts/budgets/base_contract.rb | 66 +++++++++++++++++++ .../app/contracts/budgets/create_contract.rb | 34 ++++++++++ .../app/contracts/budgets/update_contract.rb | 34 ++++++++++ .../app/controllers/budgets_controller.rb | 45 +++++-------- .../app/services/budgets/create_service.rb | 33 ++++++++++ .../budgets/set_attributes_service.rb | 65 ++++++++++++++++++ .../app/services/budgets/update_service.rb | 35 ++++++++++ .../services/budgets/create_service_spec.rb | 36 ++++++++++ .../services/budgets/update_service_spec.rb | 36 ++++++++++ 9 files changed, 356 insertions(+), 28 deletions(-) create mode 100644 modules/budgets/app/contracts/budgets/base_contract.rb create mode 100644 modules/budgets/app/contracts/budgets/create_contract.rb create mode 100644 modules/budgets/app/contracts/budgets/update_contract.rb create mode 100644 modules/budgets/app/services/budgets/create_service.rb create mode 100644 modules/budgets/app/services/budgets/set_attributes_service.rb create mode 100644 modules/budgets/app/services/budgets/update_service.rb create mode 100644 modules/budgets/spec/services/budgets/create_service_spec.rb create mode 100644 modules/budgets/spec/services/budgets/update_service_spec.rb diff --git a/modules/budgets/app/contracts/budgets/base_contract.rb b/modules/budgets/app/contracts/budgets/base_contract.rb new file mode 100644 index 0000000000..74f7716360 --- /dev/null +++ b/modules/budgets/app/contracts/budgets/base_contract.rb @@ -0,0 +1,66 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Budgets + class BaseContract < ::ModelContract + include Attachments::ValidateReplacements + + def self.model + Budget + end + + attribute :subject + attribute :description + attribute :fixed_date + attribute :project + attribute :author + attribute :new_material_budget_item_attributes, + readable: false + + attribute :new_labor_budget_item_attributes, + readable: false + + attribute :existing_material_budget_item_attributes, + readable: false + + attribute :existing_labor_budget_item_attributes, + readable: false + + validate :validate_manage_allowed + + private + + def validate_manage_allowed + unless user.allowed_to?(:edit_budgets, model.project) + errors.add :base, :error_unauthorized + end + end + end +end diff --git a/modules/budgets/app/contracts/budgets/create_contract.rb b/modules/budgets/app/contracts/budgets/create_contract.rb new file mode 100644 index 0000000000..8708e9c74c --- /dev/null +++ b/modules/budgets/app/contracts/budgets/create_contract.rb @@ -0,0 +1,34 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Budgets + class CreateContract < BaseContract + end +end diff --git a/modules/budgets/app/contracts/budgets/update_contract.rb b/modules/budgets/app/contracts/budgets/update_contract.rb new file mode 100644 index 0000000000..44a3337b21 --- /dev/null +++ b/modules/budgets/app/contracts/budgets/update_contract.rb @@ -0,0 +1,34 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Budgets + class UpdateContract < BaseContract + end +end diff --git a/modules/budgets/app/controllers/budgets_controller.rb b/modules/budgets/app/controllers/budgets_controller.rb index bb26ce9220..7127ba4ced 100644 --- a/modules/budgets/app/controllers/budgets_controller.rb +++ b/modules/budgets/app/controllers/budgets_controller.rb @@ -27,6 +27,8 @@ #++ class BudgetsController < ApplicationController + include AttachableServiceCall + before_action :find_budget, only: %i[show edit update copy] before_action :find_budgets, only: :destroy before_action :find_project, only: %i[new create update_material_budget_item update_labor_budget_item] @@ -85,11 +87,12 @@ class BudgetsController < ApplicationController def copy source = Budget.find(params[:id].to_i) - @budget = if source - Budget.new_copy(source) - else - Budget.new - end + @budget = + if source + Budget.new_copy(source) + else + Budget.new + end @budget.fixed_date ||= Date.today @@ -97,20 +100,11 @@ class BudgetsController < ApplicationController end def create - @budget = Budget.new - @budget.project_id = @project.id - - # fixed_date must be set before material_budget_items and labor_budget_items - @budget.fixed_date = if params[:budget] && params[:budget][:fixed_date] - params[:budget].delete(:fixed_date) - else - Date.today - end - - @budget.attributes = permitted_params.budget - @budget.attach_files(permitted_params.attachments.to_h) + call = attachable_create_call ::Budgets::CreateService, + args: permitted_params.budget.merge(project: @project) + @budget = call.result - if @budget.save + if call.success? flash[:notice] = t(:notice_successful_create) redirect_to(params[:continue] ? { action: 'new' } : { action: 'show', id: @budget }) else @@ -123,20 +117,15 @@ class BudgetsController < ApplicationController end def update - @budget.attributes = permitted_params.budget if params[:budget] - if params[:budget][:existing_material_budget_item_attributes].nil? - @budget.existing_material_budget_item_attributes = ({}) - end - if params[:budget][:existing_labor_budget_item_attributes].nil? - @budget.existing_labor_budget_item_attributes = ({}) - end - - @budget.attach_files(permitted_params.attachments.to_h) + call = attachable_update_call ::Budgets::UpdateService, + model: @budget, + args: permitted_params.budget - if @budget.save + if call.success? flash[:notice] = t(:notice_successful_update) redirect_to(params[:back_to] || { action: 'show', id: @budget }) else + @budget = call.result render action: 'edit' end rescue ActiveRecord::StaleObjectError diff --git a/modules/budgets/app/services/budgets/create_service.rb b/modules/budgets/app/services/budgets/create_service.rb new file mode 100644 index 0000000000..2125f3fc67 --- /dev/null +++ b/modules/budgets/app/services/budgets/create_service.rb @@ -0,0 +1,33 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Budgets + class CreateService < ::BaseServices::Create + include Attachments::ReplaceAttachments + end +end diff --git a/modules/budgets/app/services/budgets/set_attributes_service.rb b/modules/budgets/app/services/budgets/set_attributes_service.rb new file mode 100644 index 0000000000..71481e2dd6 --- /dev/null +++ b/modules/budgets/app/services/budgets/set_attributes_service.rb @@ -0,0 +1,65 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Budgets + class SetAttributesService < ::BaseServices::SetAttributes + include Attachments::SetReplacements + + private + + def set_attributes(params) + set_fixed_date(params) + unset_items(params) + + super + end + + def set_default_attributes(_params) + model.change_by_system do + model.author = user + end + end + + # fixed_date must be set before material_budget_items and labor_budget_items + def set_fixed_date(params) + model.fixed_date = params.delete(:fixed_date) || Date.today + end + + def unset_items(params) + if params[:existing_material_budget_item_attributes].nil? + model.existing_material_budget_item_attributes = ({}) + end + + if params[:existing_labor_budget_item_attributes].nil? + model.existing_labor_budget_item_attributes = ({}) + end + end + end +end diff --git a/modules/budgets/app/services/budgets/update_service.rb b/modules/budgets/app/services/budgets/update_service.rb new file mode 100644 index 0000000000..dfa73c33a9 --- /dev/null +++ b/modules/budgets/app/services/budgets/update_service.rb @@ -0,0 +1,35 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Budgets + class UpdateService < ::BaseServices::Update + include Attachments::ReplaceAttachments + end +end diff --git a/modules/budgets/spec/services/budgets/create_service_spec.rb b/modules/budgets/spec/services/budgets/create_service_spec.rb new file mode 100644 index 0000000000..4f4ba1949e --- /dev/null +++ b/modules/budgets/spec/services/budgets/create_service_spec.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require 'services/base_services/behaves_like_create_service' + +describe Budgets::CreateService, type: :model do + it_behaves_like 'BaseServices create service' +end diff --git a/modules/budgets/spec/services/budgets/update_service_spec.rb b/modules/budgets/spec/services/budgets/update_service_spec.rb new file mode 100644 index 0000000000..42051f9115 --- /dev/null +++ b/modules/budgets/spec/services/budgets/update_service_spec.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require 'services/base_services/behaves_like_update_service' + +describe Budgets::UpdateService, type: :model do + it_behaves_like 'BaseServices update service' +end From efa3ada976adb12f376e58ae987d8c4e386f17d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 8 Jul 2021 10:30:50 +0200 Subject: [PATCH 34/68] Allow options to be passed to property twin --- app/contracts/base_contract.rb | 2 +- modules/budgets/app/contracts/budgets/base_contract.rb | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/contracts/base_contract.rb b/app/contracts/base_contract.rb index be5bbe32ef..ed2c12951f 100644 --- a/app/contracts/base_contract.rb +++ b/app/contracts/base_contract.rb @@ -82,7 +82,7 @@ class BaseContract < Disposable::Twin end def attribute(attribute, options = {}, &block) - property attribute + property attribute, options.slice(:readable) add_writable(attribute, options[:writeable]) attribute_permission(attribute, options[:permission]) diff --git a/modules/budgets/app/contracts/budgets/base_contract.rb b/modules/budgets/app/contracts/budgets/base_contract.rb index 74f7716360..ece6c30e1f 100644 --- a/modules/budgets/app/contracts/budgets/base_contract.rb +++ b/modules/budgets/app/contracts/budgets/base_contract.rb @@ -40,7 +40,6 @@ module Budgets attribute :description attribute :fixed_date attribute :project - attribute :author attribute :new_material_budget_item_attributes, readable: false From e0f540ad9fe115ffd3c7d2bf76acde15f74fd6eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 8 Jul 2021 10:36:13 +0200 Subject: [PATCH 35/68] Remove setting author on budget initialize --- modules/budgets/app/models/budget.rb | 5 ----- modules/budgets/spec/models/budget_spec.rb | 10 ---------- 2 files changed, 15 deletions(-) diff --git a/modules/budgets/app/models/budget.rb b/modules/budgets/app/models/budget.rb index 47b731947a..998c43f10f 100644 --- a/modules/budgets/app/models/budget.rb +++ b/modules/budgets/app/models/budget.rb @@ -97,11 +97,6 @@ class Budget < ApplicationRecord end end - def initialize(attributes = nil) - super - self.author = User.current if new_record? - end - def budget material_budget + labor_budget end diff --git a/modules/budgets/spec/models/budget_spec.rb b/modules/budgets/spec/models/budget_spec.rb index 4aed89166c..a3bb498b0a 100644 --- a/modules/budgets/spec/models/budget_spec.rb +++ b/modules/budgets/spec/models/budget_spec.rb @@ -34,16 +34,6 @@ describe Budget, type: :model do let(:project) { FactoryBot.create(:project_with_types) } let(:user) { FactoryBot.create(:user) } - describe 'initialization' do - let(:budget) { Budget.new } - - before do - allow(User).to receive(:current).and_return(user) - end - - it { expect(budget.author).to eq(user) } - end - describe 'destroy' do let(:work_package) { FactoryBot.create(:work_package, project: project) } From abec97e1e408b8b55b012a19e17b9abf9edc8732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 8 Jul 2021 10:51:43 +0200 Subject: [PATCH 36/68] Replace meetings update with services --- .../meeting_contents/base_contract.rb | 55 +++++++++++++++++++ .../meeting_contents/update_contract.rb | 43 +++++++++++++++ .../meeting_contents_controller.rb | 35 +++++------- .../set_attributes_service.rb | 42 ++++++++++++++ .../meeting_contents/update_service.rb | 47 ++++++++++++++++ .../meeting_contents/update_contract_spec.rb | 55 +++++++++++++++++++ .../meeting_contents/update_service_spec.rb | 38 +++++++++++++ 7 files changed, 295 insertions(+), 20 deletions(-) create mode 100644 modules/meeting/app/contracts/meeting_contents/base_contract.rb create mode 100644 modules/meeting/app/contracts/meeting_contents/update_contract.rb create mode 100644 modules/meeting/app/services/meeting_contents/set_attributes_service.rb create mode 100644 modules/meeting/app/services/meeting_contents/update_service.rb create mode 100644 modules/meeting/spec/contracts/meeting_contents/update_contract_spec.rb create mode 100644 modules/meeting/spec/services/meeting_contents/update_service_spec.rb diff --git a/modules/meeting/app/contracts/meeting_contents/base_contract.rb b/modules/meeting/app/contracts/meeting_contents/base_contract.rb new file mode 100644 index 0000000000..ed749bf262 --- /dev/null +++ b/modules/meeting/app/contracts/meeting_contents/base_contract.rb @@ -0,0 +1,55 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module MeetingContents + class BaseContract < ::ModelContract + include Attachments::ValidateReplacements + + def self.model + MeetingContent + end + + attribute :meeting + attribute :text + attribute :lock_version + attribute :locked + attribute :type + + validate :type_in_allowed + + private + + def type_in_allowed + unless [MeetingAgenda.name, MeetingMinutes.name].include?(model.type) + errors.add(:type, :inclusion) + end + end + end +end diff --git a/modules/meeting/app/contracts/meeting_contents/update_contract.rb b/modules/meeting/app/contracts/meeting_contents/update_contract.rb new file mode 100644 index 0000000000..605d58ff16 --- /dev/null +++ b/modules/meeting/app/contracts/meeting_contents/update_contract.rb @@ -0,0 +1,43 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module MeetingContents + class UpdateContract < BaseContract + validate :validate_editable + + private + + def validate_editable + unless model.editable? + errors.add :base, :error_readonly + end + end + end +end diff --git a/modules/meeting/app/controllers/meeting_contents_controller.rb b/modules/meeting/app/controllers/meeting_contents_controller.rb index dd89619493..9ff36aca03 100644 --- a/modules/meeting/app/controllers/meeting_contents_controller.rb +++ b/modules/meeting/app/controllers/meeting_contents_controller.rb @@ -27,6 +27,7 @@ #++ class MeetingContentsController < ApplicationController + include AttachableServiceCall include PaginationHelper menu_item :meetings @@ -60,26 +61,26 @@ class MeetingContentsController < ApplicationController end def update - (render_403; return) unless @content.editable? # TODO: not tested! - @content.attributes = content_params.merge(author: User.current) - @content.attach_files(permitted_params.attachments.to_h) - - if !@content.lock_version_changed? - if @content.save - flash[:notice] = I18n.t(:notice_successful_update) - redirect_back_or_default controller: '/meetings', action: 'show', id: @meeting - end + call = attachable_update_call ::MeetingContents::UpdateService, + model: @content, + args: content_params + + if call.success? + flash[:notice] = I18n.t(:notice_successful_update) + redirect_back_or_default controller: '/meetings', action: 'show', id: @meeting else - render_conflict + flash.now[:error] = call.message + params[:tab] ||= 'minutes' if @meeting.agenda.present? && @meeting.agenda.locked? + render 'meetings/show' end end def history # don't load text @content_versions = @content.journals.select('id, user_id, notes, created_at, version') - .order(Arel.sql('version DESC')) - .page(page_param) - .per_page(per_page_param) + .order(Arel.sql('version DESC')) + .page(page_param) + .per_page(per_page_param) render 'meeting_contents/history', layout: !request.xhr? end @@ -129,19 +130,13 @@ class MeetingContentsController < ApplicationController def find_meeting @meeting = Meeting.includes(:project, :author, :participants, :agenda, :minutes) - .find(params[:meeting_id]) + .find(params[:meeting_id]) @project = @meeting.project @author = User.current rescue ActiveRecord::RecordNotFound render_404 end - def render_conflict - flash.now[:error] = I18n.t(:notice_locking_conflict) - params[:tab] ||= 'minutes' if @meeting.agenda.present? && @meeting.agenda.locked? - render 'meetings/show' - end - def content_params params.require(@content_type).permit(:text, :lock_version, :comment) end diff --git a/modules/meeting/app/services/meeting_contents/set_attributes_service.rb b/modules/meeting/app/services/meeting_contents/set_attributes_service.rb new file mode 100644 index 0000000000..321ea665fd --- /dev/null +++ b/modules/meeting/app/services/meeting_contents/set_attributes_service.rb @@ -0,0 +1,42 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module MeetingContents + class SetAttributesService < ::BaseServices::SetAttributes + include Attachments::SetReplacements + + + def set_default_attributes(_params) + model.change_by_system do + model.author = user if model.author.nil? + end + end + end +end diff --git a/modules/meeting/app/services/meeting_contents/update_service.rb b/modules/meeting/app/services/meeting_contents/update_service.rb new file mode 100644 index 0000000000..8eb1395733 --- /dev/null +++ b/modules/meeting/app/services/meeting_contents/update_service.rb @@ -0,0 +1,47 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module MeetingContents + class UpdateService < ::BaseServices::Update + include Attachments::ReplaceAttachments + + def persist(call) + content = call.result + + if content.lock_version_changed? + call.errors.add(:base, I18n.t(:notice_locking_conflict)) + call.success = false + return call + end + + super + end + end +end diff --git a/modules/meeting/spec/contracts/meeting_contents/update_contract_spec.rb b/modules/meeting/spec/contracts/meeting_contents/update_contract_spec.rb new file mode 100644 index 0000000000..3ebddead89 --- /dev/null +++ b/modules/meeting/spec/contracts/meeting_contents/update_contract_spec.rb @@ -0,0 +1,55 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require 'contracts/shared/model_contract_shared_context' + +describe MeetingContents::UpdateContract do + include_context 'ModelContract shared context' + let(:agenda) { FactoryBot.build_stubbed(:meeting_agenda) } + let(:current_user) { FactoryBot.build_stubbed(:user) } + let(:contract) { described_class.new(agenda, current_user) } + + context 'when not editable' do + before do + allow(agenda).to receive(:editable?).and_return false + end + + it_behaves_like 'contract is invalid', base: :error_readonly + end + + context 'when editable' do + before do + allow(agenda).to receive(:editable?).and_return true + end + + it_behaves_like 'contract is valid' + end +end diff --git a/modules/meeting/spec/services/meeting_contents/update_service_spec.rb b/modules/meeting/spec/services/meeting_contents/update_service_spec.rb new file mode 100644 index 0000000000..11dc41cc59 --- /dev/null +++ b/modules/meeting/spec/services/meeting_contents/update_service_spec.rb @@ -0,0 +1,38 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require 'services/base_services/behaves_like_update_service' + +describe MeetingContents::UpdateService, type: :model do + it_behaves_like 'BaseServices update service' do + let(:factory) { :meeting_agenda } + end +end From 582579729edf0b1269e6601b4edae1b634b301f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 8 Jul 2021 16:26:48 +0200 Subject: [PATCH 37/68] Replace ifc models attachment handling with services --- app/services/attachments/build_service.rb | 42 +++++++++++++++++++ app/services/attachments/create_service.rb | 1 + .../app/models/bim/ifc_models/ifc_model.rb | 10 ++++- .../bim/ifc_models/set_attributes_service.rb | 8 +++- .../bim/spec/bcf/bcf_xml/issue_writer_spec.rb | 7 ++-- .../bim/spec/factories/ifc_model_factory.rb | 35 +++++++++++----- .../bim/spec/features/bim_navigation_spec.rb | 9 ++-- .../spec/features/model_management_spec.rb | 4 +- .../bim/spec/features/model_viewer_spec.rb | 4 +- modules/bim/spec/models/ifc_model_spec.rb | 10 ++++- .../ifc_models/set_attributes_service_spec.rb | 15 ++++--- 11 files changed, 114 insertions(+), 31 deletions(-) create mode 100644 app/services/attachments/build_service.rb diff --git a/app/services/attachments/build_service.rb b/app/services/attachments/build_service.rb new file mode 100644 index 0000000000..937efe160d --- /dev/null +++ b/app/services/attachments/build_service.rb @@ -0,0 +1,42 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Attachments + class BuildService < ::BaseServices::Create + private + + def persist(service_result) + unless service_result.result.valid? + service_result.errors = service_result.result.errors + service_result.success = false + end + + service_result + end + end +end diff --git a/app/services/attachments/create_service.rb b/app/services/attachments/create_service.rb index 33f00f1586..772b5ef3f2 100644 --- a/app/services/attachments/create_service.rb +++ b/app/services/attachments/create_service.rb @@ -68,6 +68,7 @@ class Attachments::CreateService < ::BaseServices::Create def error_wrapped_call yield rescue StandardError => e + binding.pry log_attachment_saving_error(e) message = diff --git a/modules/bim/app/models/bim/ifc_models/ifc_model.rb b/modules/bim/app/models/bim/ifc_models/ifc_model.rb index 1ae55c12ca..807606a8bd 100644 --- a/modules/bim/app/models/bim/ifc_models/ifc_model.rb +++ b/modules/bim/app/models/bim/ifc_models/ifc_model.rb @@ -1,7 +1,9 @@ module Bim module IfcModels class IfcModel < ActiveRecord::Base - acts_as_attachable delete_permission: :manage_ifc_models, view_permission: :view_ifc_models + acts_as_attachable delete_permission: :manage_ifc_models, + add_permission: :manage_ifc_models, + view_permission: :view_ifc_models belongs_to :project belongs_to :uploader, class_name: 'User', foreign_key: 'uploader_id' @@ -23,7 +25,11 @@ module Bim end delete_attachment name - attach_files('first' => { 'file' => file, 'description' => name }) + call = ::Attachments::CreateService + .new(user: User.current) + .call(file: file, container: self, filename: file.original_filename, description: name) + + call.on_failure { Rails.logger.error "Failed to add #{name} attachment: #{call.message}" } end end diff --git a/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb b/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb index 80661c043b..0c070c8969 100644 --- a/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb +++ b/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb @@ -39,7 +39,7 @@ module Bim super model.change_by_system do - model.uploader = model.ifc_attachment&.author if model.ifc_attachment&.new_record? || model.ifc_attachment&.pending_direct_upload? + model.uploader = model.ifc_attachment&.author end end @@ -75,7 +75,11 @@ module Bim model.attachments << ifc_attachment else - model.attach_files('first' => { 'file' => ifc_attachment, 'description' => 'ifc' }) + call = ::Attachments::BuildService + .new(user: user) + .call(file: ifc_attachment, filename: ifc_attachment.original_filename, description: 'ifc') + + model.association(:attachments).add_to_target(call.result) end end end diff --git a/modules/bim/spec/bcf/bcf_xml/issue_writer_spec.rb b/modules/bim/spec/bcf/bcf_xml/issue_writer_spec.rb index de0e0c659e..f23db8f793 100644 --- a/modules/bim/spec/bcf/bcf_xml/issue_writer_spec.rb +++ b/modules/bim/spec/bcf/bcf_xml/issue_writer_spec.rb @@ -103,9 +103,7 @@ describe ::OpenProject::Bim::BcfXml::IssueWriter do before do allow(User).to receive(:current).and_return current_user - - bcf_issue.comments.first.journal.update_attribute('journable_id', work_package.id) - FactoryBot.create(:work_package_journal, notes: "Some note created in OP.", journable_id: work_package.id) + bcf_issue.comments.first.journal.update_columns(journable_id: work_package.id, version: 2) end shared_examples_for "writes Topic" do @@ -174,6 +172,9 @@ describe ::OpenProject::Bim::BcfXml::IssueWriter do end it 'creates BCF comments for comments that were created within OP.' do + work_package.journal_notes = 'Some note created in OP.' + work_package.save! + expect(subject.at('/Markup/Comment[2]/Comment').content).to eql("Some note created in OP.") expect(Bim::Bcf::Comment.count).to eql(2) end diff --git a/modules/bim/spec/factories/ifc_model_factory.rb b/modules/bim/spec/factories/ifc_model_factory.rb index 3162fd232c..afa99ca606 100644 --- a/modules/bim/spec/factories/ifc_model_factory.rb +++ b/modules/bim/spec/factories/ifc_model_factory.rb @@ -32,11 +32,19 @@ FactoryBot.define do project factory: :project uploader factory: :user is_default { true } - ifc_attachment do - Rack::Test::UploadedFile.new( - File.join(Rails.root, "modules/bim/spec/fixtures/files/minimal.ifc"), - 'application/binary' - ) + transient do + ifc_attachment do + Rack::Test::UploadedFile.new( + File.join(Rails.root, "modules/bim/spec/fixtures/files/minimal.ifc"), + 'application/binary' + ) + end + + callback(:after_create) do |model, evaluator| + User.system.run_given do + model.ifc_attachment = evaluator.ifc_attachment + end + end end factory :ifc_model_minimal_converted do @@ -44,12 +52,19 @@ FactoryBot.define do project factory: :project uploader factory: :user is_default { true } + transient do + xkt_attachment do + Rack::Test::UploadedFile.new( + File.join(Rails.root, "modules/bim/spec/fixtures/files/minimal.xkt"), + 'application/binary' + ) + end + end - xkt_attachment do - Rack::Test::UploadedFile.new( - File.join(Rails.root, "modules/bim/spec/fixtures/files/minimal.xkt"), - 'application/binary' - ) + callback(:after_create) do |model, evaluator| + User.system.run_given do + model.xkt_attachment = evaluator.xkt_attachment + end end end end diff --git a/modules/bim/spec/features/bim_navigation_spec.rb b/modules/bim/spec/features/bim_navigation_spec.rb index e444666caf..0f2a3eb006 100644 --- a/modules/bim/spec/features/bim_navigation_spec.rb +++ b/modules/bim/spec/features/bim_navigation_spec.rb @@ -44,7 +44,7 @@ describe 'BIM navigation spec', member_through_role: role end - let!(:model) do + let(:model) do FactoryBot.create(:ifc_model_minimal_converted, project: project, uploader: user) @@ -56,16 +56,19 @@ describe 'BIM navigation spec', let(:model_tree) { ::Components::XeokitModelTree.new } let(:destroy_modal) { Components::WorkPackages::DestroyModal.new } + before do + login_as user + model + end + shared_examples 'can switch from split to viewer to list-only' do before do - login_as(user) model_page.visit! model_page.finished_loading end context 'deep link on the page' do before do - login_as(user) model_page.visit! model_page.finished_loading diff --git a/modules/bim/spec/features/model_management_spec.rb b/modules/bim/spec/features/model_management_spec.rb index 79049dce54..e45344f757 100644 --- a/modules/bim/spec/features/model_management_spec.rb +++ b/modules/bim/spec/features/model_management_spec.rb @@ -44,14 +44,14 @@ describe 'model management', member_through_role: role end - let(:model) do + let!(:model) do FactoryBot.create(:ifc_model_minimal_converted, project: project, uploader: user, is_default: true) end - let(:model2) do + let!(:model2) do FactoryBot.create(:ifc_model_minimal_converted, project: project, uploader: user) diff --git a/modules/bim/spec/features/model_viewer_spec.rb b/modules/bim/spec/features/model_viewer_spec.rb index 44ea7c9868..66eb1932a8 100644 --- a/modules/bim/spec/features/model_viewer_spec.rb +++ b/modules/bim/spec/features/model_viewer_spec.rb @@ -43,7 +43,7 @@ describe 'model viewer', member_through_role: role end - let(:model) do + let!(:model) do FactoryBot.create(:ifc_model_minimal_converted, project: project, uploader: user) @@ -92,6 +92,8 @@ describe 'model viewer', end context 'in a project with no model' do + let!(:model) { nil } + it 'shows a warning that no IFC models exist yet' do login_as user visit defaults_bcf_project_ifc_models_path(project) diff --git a/modules/bim/spec/models/ifc_model_spec.rb b/modules/bim/spec/models/ifc_model_spec.rb index 4dc28cbc1d..6542dc65d6 100644 --- a/modules/bim/spec/models/ifc_model_spec.rb +++ b/modules/bim/spec/models/ifc_model_spec.rb @@ -16,11 +16,17 @@ describe ::Bim::IfcModels::IfcModel, type: :model do end describe 'ifc_attachment=' do - subject { FactoryBot.create :ifc_model_minimal_converted, project: FactoryBot.create(:project) } + let(:project) { FactoryBot.create(:project, enabled_module_names: %i[bim]) } let(:ifc_attachment) { subject.ifc_attachment } let(:new_attachment) do FileHelpers.mock_uploaded_file name: "model.ifc", content_type: 'application/binary', binary: true end + subject { FactoryBot.create :ifc_model_minimal_converted, project: project } + current_user do + FactoryBot.create :user, + member_in_project: project, + member_with_permissions: %i[manage_ifc_models] + end it 'replaces the previous attachment' do expect(ifc_attachment).to be_present @@ -31,7 +37,7 @@ describe ::Bim::IfcModels::IfcModel, type: :model do expect { ifc_attachment.reload }.to raise_error(ActiveRecord::RecordNotFound) expect(subject.ifc_attachment).not_to eq ifc_attachment - expect(subject.ifc_attachment).not_to be_present + expect(subject.ifc_attachment).to be_present expect(subject.xkt_attachment).not_to be_present expect(subject).not_to be_converted end diff --git a/modules/bim/spec/services/ifc_models/set_attributes_service_spec.rb b/modules/bim/spec/services/ifc_models/set_attributes_service_spec.rb index 533d859149..fdfbf353a2 100644 --- a/modules/bim/spec/services/ifc_models/set_attributes_service_spec.rb +++ b/modules/bim/spec/services/ifc_models/set_attributes_service_spec.rb @@ -31,7 +31,10 @@ require 'spec_helper' describe Bim::IfcModels::SetAttributesService, type: :model do - let(:user) { FactoryBot.build_stubbed(:user) } + shared_let(:project) { FactoryBot.create(:project, enabled_module_names: %i[bim]) } + shared_let(:other_project) { FactoryBot.create(:project, enabled_module_names: %i[bim]) } + shared_let(:user) { FactoryBot.create(:user, member_in_project: project, member_with_permissions: %i[manage_ifc_models]) } + let(:other_user) { FactoryBot.build_stubbed(:user) } let(:contract_class) do contract = double('contract_class') @@ -58,10 +61,8 @@ describe Bim::IfcModels::SetAttributesService, type: :model do end let(:call_attributes) { {} } let(:ifc_file) { FileHelpers.mock_uploaded_file(name: "model_2.ifc", content_type: 'application/binary', binary: true) } - let(:ifc_attachment) { FactoryBot.build_stubbed(:attachment) } - let(:project) { FactoryBot.build_stubbed(:project) } let(:model) do - FactoryBot.build_stubbed(:ifc_model, attachments: [ifc_attachment], uploader: other_user) + FactoryBot.create(:ifc_model, project: project, uploader: other_user) end before do @@ -72,7 +73,7 @@ describe Bim::IfcModels::SetAttributesService, type: :model do describe 'call' do let(:call_attributes) do { - project_id: project.id + project_id: other_project.id } end @@ -96,7 +97,7 @@ describe Bim::IfcModels::SetAttributesService, type: :model do subject expect(model.attributes.slice(*model.changed).symbolize_keys) - .to eql call_attributes + .to eql call_attributes.merge(uploader_id: user.id) end it 'does not persist the model' do @@ -167,6 +168,8 @@ describe Bim::IfcModels::SetAttributesService, type: :model do end it 'marks existing attachments for destruction' do + ifc_attachment = model.ifc_attachment + subject expect(ifc_attachment) From f64312133890822fc0346f9a22901604b4113e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 8 Jul 2021 20:15:51 +0200 Subject: [PATCH 38/68] Don't check uploader if changed by system --- modules/bim/app/contracts/bim/ifc_models/base_contract.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/bim/app/contracts/bim/ifc_models/base_contract.rb b/modules/bim/app/contracts/bim/ifc_models/base_contract.rb index d98bf621ef..ac7e0a55e9 100644 --- a/modules/bim/app/contracts/bim/ifc_models/base_contract.rb +++ b/modules/bim/app/contracts/bim/ifc_models/base_contract.rb @@ -54,7 +54,9 @@ module Bim end def user_is_uploader - if model.uploader_id_changed? && model.uploader != user + return unless changed_by_user.include?('uploader_id') + + if model.uploader != user errors.add :uploader_id, :invalid end end From 8a881cb20cde825ac163d38f377d30eff110f058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 8 Jul 2021 20:38:51 +0200 Subject: [PATCH 39/68] Fix uploader being changed by system --- .../contracts/bim/ifc_models/base_contract.rb | 9 ---- .../ifc_models/shared_contract_examples.rb | 44 +++++++++---------- .../ifc_models/update_contract_spec.rb | 25 ++++++++--- 3 files changed, 38 insertions(+), 40 deletions(-) diff --git a/modules/bim/app/contracts/bim/ifc_models/base_contract.rb b/modules/bim/app/contracts/bim/ifc_models/base_contract.rb index ac7e0a55e9..2dfbf4dabd 100644 --- a/modules/bim/app/contracts/bim/ifc_models/base_contract.rb +++ b/modules/bim/app/contracts/bim/ifc_models/base_contract.rb @@ -42,7 +42,6 @@ module Bim end validate :user_allowed_to_manage - validate :user_is_uploader validate :ifc_attachment_existent validate :ifc_attachment_is_ifc validate :uploader_is_ifc_attachment_author @@ -53,14 +52,6 @@ module Bim end end - def user_is_uploader - return unless changed_by_user.include?('uploader_id') - - if model.uploader != user - errors.add :uploader_id, :invalid - end - end - def ifc_attachment_existent errors.add :base, :ifc_attachment_missing unless model.ifc_attachment end diff --git a/modules/bim/spec/contracts/ifc_models/shared_contract_examples.rb b/modules/bim/spec/contracts/ifc_models/shared_contract_examples.rb index 82936c8184..d16b8d0adf 100644 --- a/modules/bim/spec/contracts/ifc_models/shared_contract_examples.rb +++ b/modules/bim/spec/contracts/ifc_models/shared_contract_examples.rb @@ -31,22 +31,8 @@ require 'spec_helper' shared_examples_for 'ifc model contract' do - let(:current_user) do - FactoryBot.build_stubbed(:user) do |user| - allow(user) - .to receive(:allowed_to?) do |permission, permission_project| - permissions.include?(permission) && model_project == permission_project - end - end - end - let(:other_user) do - FactoryBot.build_stubbed(:user) do |user| - allow(user) - .to receive(:allowed_to?) do |permission, permission_project| - permissions.include?(permission) && model_project == permission_project - end - end - end + let(:current_user) { FactoryBot.build_stubbed(:user) } + let(:other_user) { FactoryBot.build_stubbed(:user) } let(:model_project) { FactoryBot.build_stubbed(:project) } let(:ifc_attachment) { FactoryBot.build_stubbed(:attachment, author: model_user) } let(:model_user) { current_user } @@ -56,6 +42,16 @@ shared_examples_for 'ifc model contract' do allow(ifc_model) .to receive(:ifc_attachment) .and_return(ifc_attachment) + + allow(other_user) + .to receive(:allowed_to?) do |permission, permission_project| + permissions.include?(permission) && model_project == permission_project + end + + allow(current_user) + .to receive(:allowed_to?) do |permission, permission_project| + permissions.include?(permission) && model_project == permission_project + end end def expect_valid(valid, symbols = {}) @@ -109,10 +105,10 @@ shared_examples_for 'ifc model contract' do context 'if the new ifc file is no valid ifc file' do let(:ifc_file) { FileHelpers.mock_uploaded_file name: "model.ifc", content_type: 'application/binary', binary: true } let(:ifc_attachment) do - User.execute_as current_user do - ifc_model.attach_files('first' => { 'file' => ifc_file, 'description' => 'ifc' }) - ifc_model.attachments.last - end + ::Attachments::BuildService + .new(user: current_user) + .call(file: ifc_file, filename: 'model.ifc') + .result end it 'is invalid' do @@ -125,10 +121,10 @@ shared_examples_for 'ifc model contract' do FileHelpers.mock_uploaded_file name: "model.ifc", content_type: 'application/binary', binary: true, content: "ISO-10303-21;" end let(:ifc_attachment) do - User.execute_as current_user do - ifc_model.attach_files('first' => { 'file' => ifc_file, 'description' => 'ifc' }) - ifc_model.attachments.last - end + ::Attachments::BuildService + .new(user: current_user) + .call(file: ifc_file, filename: 'model.ifc') + .result end it_behaves_like 'is valid' diff --git a/modules/bim/spec/contracts/ifc_models/update_contract_spec.rb b/modules/bim/spec/contracts/ifc_models/update_contract_spec.rb index afa42d0ced..84d71c019b 100644 --- a/modules/bim/spec/contracts/ifc_models/update_contract_spec.rb +++ b/modules/bim/spec/contracts/ifc_models/update_contract_spec.rb @@ -42,13 +42,18 @@ describe Bim::IfcModels::UpdateContract do project: model_project).tap do |model| model.extend(OpenProject::ChangedBySystem) - model.change_by_system do + if changed_by_system + changed_by_system do + model.uploader = uploader_user + end + else model.uploader = uploader_user end end end let(:permissions) { %i(manage_ifc_models) } let(:uploader_user) { model_user } + let(:changed_by_system) { false } context 'if the uploader changes' do let(:model_user) { FactoryBot.build_stubbed(:user) } @@ -56,14 +61,20 @@ describe Bim::IfcModels::UpdateContract do let(:current_user) { other_user } let(:ifc_attachment) { FactoryBot.build_stubbed(:attachment, author: other_user) } - it_behaves_like 'is valid' + it 'is invalid as not writable' do + expect_valid(false, uploader_id: %i(error_readonly)) + end + end - context 'if the uploader is different from the current_user' do - let(:current_user) { model_user } + context 'if the uploader changes' do + let(:model_user) { FactoryBot.build_stubbed(:user) } + let(:uploader_user) { other_user } + let(:current_user) { other_user } + let(:ifc_attachment) { FactoryBot.build_stubbed(:attachment, author: other_user) } + let(:changed_by_system) { true } - it 'is invalid' do - expect_valid(false, uploader_id: %i(invalid)) - end + it 'is invalid as does not match' do + expect_valid(false, uploader_id: %i(invalid)) end end From a59e4dc376f3a8e3af9609375461715d37484b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 8 Jul 2021 21:24:31 +0200 Subject: [PATCH 40/68] Replace wiki page attach_files with attachable services --- app/contracts/wiki_pages/update_contract.rb | 32 ++++++++++++ app/controllers/wiki_controller.rb | 25 +++++----- app/models/permitted_params.rb | 4 ++ app/services/attachments/create_service.rb | 1 - app/services/wiki_pages/create_service.rb | 1 + .../wiki_pages/set_attributes_service.rb | 10 +++- app/services/wiki_pages/update_service.rb | 50 +++++++++++++++++++ 7 files changed, 107 insertions(+), 16 deletions(-) create mode 100644 app/contracts/wiki_pages/update_contract.rb create mode 100644 app/services/wiki_pages/update_service.rb diff --git a/app/contracts/wiki_pages/update_contract.rb b/app/contracts/wiki_pages/update_contract.rb new file mode 100644 index 0000000000..3aa737381d --- /dev/null +++ b/app/contracts/wiki_pages/update_contract.rb @@ -0,0 +1,32 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module WikiPages + class UpdateContract < BaseContract + end +end diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index 8ceed25371..b003b9d9be 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -57,6 +57,7 @@ class WikiController < ApplicationController before_action :handle_new_wiki_page, only: %i[show] before_action :build_wiki_page_and_content, only: %i[new create] + include AttachableServiceCall include AttachmentsHelper include PaginationHelper include Redmine::MenuManager::WikiMenuHelper @@ -105,13 +106,13 @@ class WikiController < ApplicationController end def create - @page.attributes = permitted_params.wiki_page + call = attachable_create_call ::WikiPages::CreateService, + args: permitted_params.wiki_page_with_content.to_h.merge(wiki: @wiki) - @content.attributes = permitted_params.wiki_content - @content.author = User.current - @page.attach_files(permitted_params.attachments.to_h) + @page = call.result + @content = @page.content - if @page.save + if call.success? call_hook(:controller_wiki_edit_after_save, params: params, page: @page) flash[:notice] = I18n.t(:notice_successful_create) redirect_to_show @@ -165,18 +166,16 @@ class WikiController < ApplicationController return end - @content = @page.content || @page.build_content return if locked? - @page.attach_files(permitted_params.attachments.to_h) + call = attachable_update_call ::WikiPages::UpdateService, + model: @page, + args: permitted_params.wiki_page_with_content.to_h - @page.title = permitted_params.wiki_page[:title] - @page.parent_id = permitted_params.wiki_page[:parent_id].presence - @content.attributes = permitted_params.wiki_content - @content.author = User.current - @content.add_journal User.current, params['content']['comments'] + @page = call.result + @content = @page.content - if @page.save_with_content + if call.success? call_hook(:controller_wiki_edit_after_save, params: params, page: @page) flash[:notice] = I18n.t(:notice_successful_update) redirect_to_show diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb index 397d8123b2..cb242b4c6e 100644 --- a/app/models/permitted_params.rb +++ b/app/models/permitted_params.rb @@ -252,6 +252,10 @@ class PermittedParams params.require(:content).permit(*self.class.permitted_attributes[:wiki_content]) end + def wiki_page_with_content + wiki_page.merge(wiki_content) + end + def pref params.fetch(:pref, {}).permit(:hide_mail, :time_zone, :theme, :comments_sorting, :warn_on_leaving_unsaved, diff --git a/app/services/attachments/create_service.rb b/app/services/attachments/create_service.rb index 772b5ef3f2..33f00f1586 100644 --- a/app/services/attachments/create_service.rb +++ b/app/services/attachments/create_service.rb @@ -68,7 +68,6 @@ class Attachments::CreateService < ::BaseServices::Create def error_wrapped_call yield rescue StandardError => e - binding.pry log_attachment_saving_error(e) message = diff --git a/app/services/wiki_pages/create_service.rb b/app/services/wiki_pages/create_service.rb index 6c138267d6..8fd4e802d5 100644 --- a/app/services/wiki_pages/create_service.rb +++ b/app/services/wiki_pages/create_service.rb @@ -29,4 +29,5 @@ #++ class WikiPages::CreateService < ::BaseServices::Create + include Attachments::ReplaceAttachments end diff --git a/app/services/wiki_pages/set_attributes_service.rb b/app/services/wiki_pages/set_attributes_service.rb index c3b89e7c11..64fd698d34 100644 --- a/app/services/wiki_pages/set_attributes_service.rb +++ b/app/services/wiki_pages/set_attributes_service.rb @@ -36,6 +36,8 @@ # # Attributes for both the page as well as for the content are accepted. class WikiPages::SetAttributesService < ::BaseServices::SetAttributes + include Attachments::SetReplacements + private def set_attributes(params) @@ -53,7 +55,7 @@ class WikiPages::SetAttributesService < ::BaseServices::SetAttributes end def set_default_attributes(_params) - model.build_content + model.build_content page: model model.content.extend(OpenProject::ChangedBySystem) model.content.change_by_system do @@ -66,6 +68,10 @@ class WikiPages::SetAttributesService < ::BaseServices::SetAttributes end def split_page_and_content_params(params) - params.partition { |p, _| WikiContent.column_names.include?(p) }.map(&:to_h) + params.partition { |p, _| content_attribute?(p) }.map(&:to_h) + end + + def content_attribute?(name) + WikiContent.column_names.include?(name) || name.to_s == 'comments' end end diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb new file mode 100644 index 0000000000..c76009f09a --- /dev/null +++ b/app/services/wiki_pages/update_service.rb @@ -0,0 +1,50 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +class WikiPages::UpdateService < ::BaseServices::Update + include Attachments::ReplaceAttachments + + private + + def persist(service_result) + service_result = super(service_result) + + page = service_result.result + content = page.content + + unless page.save && content.save + service_result.errors = page.errors + service_result.errors.merge! content.errors + service_result.success = false + end + + service_result + end +end From 5f44166ad965e2c34e2b2eb6348ea778236fa7ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 8 Jul 2021 21:32:25 +0200 Subject: [PATCH 41/68] Replace avatar saving --- .../lib/open_project/avatars/patches/user_patch.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/avatars/lib/open_project/avatars/patches/user_patch.rb b/modules/avatars/lib/open_project/avatars/patches/user_patch.rb index f675d5874c..4b5d96b30e 100644 --- a/modules/avatars/lib/open_project/avatars/patches/user_patch.rb +++ b/modules/avatars/lib/open_project/avatars/patches/user_patch.rb @@ -60,8 +60,12 @@ module OpenProject::Avatars local_avatar_attachment&.destroy reset_avatar_attachment_cache! - attach_files('first' => { 'file' => file, 'description' => 'avatar' }) - save + @local_avatar_attachment = Attachments::CreateService + .new(user: User.system, contract_class: EmptyContract) + .call(file: file, container: self, filename: file.original_filename, description: 'avatar') + .result + + touch end def reset_avatar_attachment_cache! From 7c25932f9438f7f56b0bb5ee534ad798fd77bd2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 8 Jul 2021 21:35:39 +0200 Subject: [PATCH 42/68] Replace snapshot attach_files --- modules/bim/app/models/bim/bcf/viewpoint.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/bim/app/models/bim/bcf/viewpoint.rb b/modules/bim/app/models/bim/bcf/viewpoint.rb index 3c1cb83afc..2112850cef 100644 --- a/modules/bim/app/models/bim/bcf/viewpoint.rb +++ b/modules/bim/app/models/bim/bcf/viewpoint.rb @@ -41,7 +41,10 @@ module Bim::Bcf def snapshot=(file) snapshot&.destroy - attach_files('first' => { 'file' => file, 'description' => 'snapshot' }) + Attachments::CreateService + .new(user: User.current) + .call(file: file, container: self, filename: file.original_filename, description: 'snapshot') + .result end end end From bf30ad0d98e7575cd3bb1bea213e22d2afa76855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 8 Jul 2021 21:57:39 +0200 Subject: [PATCH 43/68] Skip double validation when container present --- app/contracts/attachments/create_contract.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/contracts/attachments/create_contract.rb b/app/contracts/attachments/create_contract.rb index 77e1a074ce..b7d8aad91b 100644 --- a/app/contracts/attachments/create_contract.rb +++ b/app/contracts/attachments/create_contract.rb @@ -49,6 +49,8 @@ module Attachments private def validate_attachments_addable + return if model.container + if Redmine::Acts::Attachable.attachables.none?(&:attachments_addable?) errors.add(:base, :error_unauthorized) end From 9e1dc1d50e4e3e54052f4910a15784eaa9e35ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 8 Jul 2021 21:57:50 +0200 Subject: [PATCH 44/68] Set snapshot through attachment service --- app/services/attachments/build_service.rb | 13 +++++++++++-- .../contracts/bim/bcf/viewpoints/create_contract.rb | 1 + modules/bim/app/models/bim/bcf/viewpoint.rb | 8 ++++++-- .../bim/bcf/viewpoints/set_attributes_service.rb | 6 ++++-- .../bim/ifc_models/set_attributes_service.rb | 6 ++---- .../bcf/viewpoints/set_attributes_service_spec.rb | 9 ++++++++- .../ifc_models/set_attributes_service_spec.rb | 2 +- 7 files changed, 33 insertions(+), 12 deletions(-) diff --git a/app/services/attachments/build_service.rb b/app/services/attachments/build_service.rb index 937efe160d..9a725f571b 100644 --- a/app/services/attachments/build_service.rb +++ b/app/services/attachments/build_service.rb @@ -31,12 +31,21 @@ module Attachments private def persist(service_result) - unless service_result.result.valid? - service_result.errors = service_result.result.errors + attachment = service_result.result + add_to_association(attachment) + + unless attachment.valid? + service_result.errors = attachment.errors service_result.success = false end service_result end + + def add_to_association(attachment) + return unless attachment.container + + attachment.container.association(:attachments).add_to_target(attachment) + end end end diff --git a/modules/bim/app/contracts/bim/bcf/viewpoints/create_contract.rb b/modules/bim/app/contracts/bim/bcf/viewpoints/create_contract.rb index 6f568b0823..7edea10890 100644 --- a/modules/bim/app/contracts/bim/bcf/viewpoints/create_contract.rb +++ b/modules/bim/app/contracts/bim/bcf/viewpoints/create_contract.rb @@ -84,6 +84,7 @@ module Bim::Bcf attribute :uuid attribute :issue + attribute :snapshot attribute :json_viewpoint do validate_json_viewpoint_present validate_json_viewpoint_hash diff --git a/modules/bim/app/models/bim/bcf/viewpoint.rb b/modules/bim/app/models/bim/bcf/viewpoint.rb index 2112850cef..aaa3bff718 100644 --- a/modules/bim/app/models/bim/bcf/viewpoint.rb +++ b/modules/bim/app/models/bim/bcf/viewpoint.rb @@ -41,8 +41,12 @@ module Bim::Bcf def snapshot=(file) snapshot&.destroy - Attachments::CreateService - .new(user: User.current) + build_snapshot file + end + + def build_snapshot(file, user: User.current) + ::Attachments::BuildService + .new(user: user) .call(file: file, container: self, filename: file.original_filename, description: 'snapshot') .result end diff --git a/modules/bim/app/services/bim/bcf/viewpoints/set_attributes_service.rb b/modules/bim/app/services/bim/bcf/viewpoints/set_attributes_service.rb index fd861246f6..c9cce4c2ac 100644 --- a/modules/bim/app/services/bim/bcf/viewpoints/set_attributes_service.rb +++ b/modules/bim/app/services/bim/bcf/viewpoints/set_attributes_service.rb @@ -44,8 +44,9 @@ module Bim::Bcf def set_snapshot return unless snapshot_data_complete? && snapshot_content_type + name = "snapshot.#{snapshot_extension}" file = OpenProject::Files - .create_uploaded_file(name: "snapshot.#{snapshot_extension}", + .create_uploaded_file(name: name, content_type: snapshot_content_type, content: snapshot_binary_contents, binary: true) @@ -54,7 +55,8 @@ module Bim::Bcf # to update existing viewpoints as the snapshot method will # delete any existing snapshot right away while the expectation # on a SetAttributesService is to not perform persisted changes. - model.snapshot = file + model.snapshot&.mark_for_destruction + model.build_snapshot file, user: user end def snapshot_data_complete? diff --git a/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb b/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb index 0c070c8969..b703ceee16 100644 --- a/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb +++ b/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb @@ -75,11 +75,9 @@ module Bim model.attachments << ifc_attachment else - call = ::Attachments::BuildService + ::Attachments::BuildService .new(user: user) - .call(file: ifc_attachment, filename: ifc_attachment.original_filename, description: 'ifc') - - model.association(:attachments).add_to_target(call.result) + .call(file: ifc_attachment, container: model, filename: ifc_attachment.original_filename, description: 'ifc') end end end diff --git a/modules/bim/spec/services/bcf/viewpoints/set_attributes_service_spec.rb b/modules/bim/spec/services/bcf/viewpoints/set_attributes_service_spec.rb index 7bfc3783d4..2a813b0b39 100644 --- a/modules/bim/spec/services/bcf/viewpoints/set_attributes_service_spec.rb +++ b/modules/bim/spec/services/bcf/viewpoints/set_attributes_service_spec.rb @@ -60,6 +60,13 @@ describe Bim::Bcf::Viewpoints::SetAttributesService, type: :model do Bim::Bcf::Viewpoint.new end + before do + allow(viewpoint) + .to receive(:attachments_addable?) + .with(user, any_args) + .and_return true + end + describe 'call' do # We only expect the service to be called for new records. As viewpoints # are immutable. @@ -113,7 +120,7 @@ describe Bim::Bcf::Viewpoints::SetAttributesService, type: :model do expect(viewpoint.attachments.first.file.read) .to eql 'Hello World!' - expect(viewpoint.attachments.first.file.filename) + expect(viewpoint.attachments.first.filename) .to eql 'snapshot.png' end diff --git a/modules/bim/spec/services/ifc_models/set_attributes_service_spec.rb b/modules/bim/spec/services/ifc_models/set_attributes_service_spec.rb index fdfbf353a2..72427087cd 100644 --- a/modules/bim/spec/services/ifc_models/set_attributes_service_spec.rb +++ b/modules/bim/spec/services/ifc_models/set_attributes_service_spec.rb @@ -109,7 +109,7 @@ describe Bim::IfcModels::SetAttributesService, type: :model do context 'for a new record' do let(:model) do - Bim::IfcModels::IfcModel.new + Bim::IfcModels::IfcModel.new project: project end context 'with an ifc_attachment' do From f6305c6a82b2de8998eb3c7d1007e15b1d6d5c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 8 Jul 2021 21:57:55 +0200 Subject: [PATCH 45/68] Remove attach_files --- .../lib/acts_as_attachable.rb | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/lib/plugins/acts_as_attachable/lib/acts_as_attachable.rb b/lib/plugins/acts_as_attachable/lib/acts_as_attachable.rb index a251638085..d1243713c1 100644 --- a/lib/plugins/acts_as_attachable/lib/acts_as_attachable.rb +++ b/lib/plugins/acts_as_attachable/lib/acts_as_attachable.rb @@ -54,8 +54,6 @@ module Redmine :attachments_claimed send :include, Redmine::Acts::Attachable::InstanceMethods - - OpenProject::Deprecation.deprecate_method self, :attach_files end private @@ -174,26 +172,6 @@ module Redmine (persisted? && allowed_to_on_attachment?(user, self.class.attachable_options[:add_on_persisted_permission])) end - # Bulk attaches a set of files to an object - # @deprecated - # Either use the already existing Attachments::CreateService or - # write/extend Services for the attached to object. - # The service should rely on the attachments_replacements variable. - # See: - # * app/services/attachments/set_replacements.rb - # * app/services/attachments/replace_attachments.rb - def attach_files(attachments) - return unless attachments&.is_a?(Hash) - - attachments.each_value do |attachment| - if attachment['file'] - build_attachments_from_hash(attachment) - elsif attachment['id'] - memoize_attachment_for_claiming(attachment) - end - end - end - private def allowed_to_on_attachment?(user, permissions) @@ -222,21 +200,6 @@ module Redmine (Redmine::Acts::Attachable.attachables & self.class.ancestors).first end - def build_attachments_from_hash(attachment_hash) - if (file = attachment_hash['file']) && file && file.size.positive? - attachments.build(file: file, - container: self, - description: attachment_hash['description'].to_s.strip, - author: User.current) - end - end - - def memoize_attachment_for_claiming(attachment_hash) - self.attachments_claimed ||= [] - attachment = Attachment.find(attachment_hash['id']) - self.attachments_claimed << attachment unless id && attachment.container_id == id - end - def validate_attachments_claimable return unless claimed_attachments? From b9875fa0c5d35a0dba62b1824458365010ddc1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Sun, 11 Jul 2021 22:07:42 +0200 Subject: [PATCH 46/68] Validate content type in contract --- app/contracts/attachments/create_contract.rb | 20 ++++++++++++++++++++ config/locales/en.yml | 4 ++++ lib/open_project/content_type_detector.rb | 18 +++++++++--------- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/app/contracts/attachments/create_contract.rb b/app/contracts/attachments/create_contract.rb index b7d8aad91b..60a7b57b13 100644 --- a/app/contracts/attachments/create_contract.rb +++ b/app/contracts/attachments/create_contract.rb @@ -45,6 +45,7 @@ module Attachments validate :validate_attachments_addable validate :validate_container_addable validate :validate_author + validate :validate_content_type private @@ -67,5 +68,24 @@ module Attachments errors.add(:base, :error_unauthorized) unless model.container.attachments_addable?(user) end + + ## + # Validates the content type, if a whitelist is set + def validate_content_type + # In some cases, we do not want to process the whitelist + # for the attachment mime types. For example, when + # storing backups or exports + return unless options.fetch(:validate_content_type, true) + + whitelist = Array(Setting.attachment_whitelist) + + # If the whitelist is empty, assume all files are allowed + # as before + return if whitelist.empty? + + unless whitelist.include?(model.content_type) || whitelist.include?("*#{model.extension}") + errors.add :content_type, :not_whitelisted, value: model.content_type + end + end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 09e42c08dc..a0a4bbe12a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -637,6 +637,10 @@ en: unremovable: "cannot be removed." wrong_length: "is the wrong length (should be %{count} characters)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Global' diff --git a/lib/open_project/content_type_detector.rb b/lib/open_project/content_type_detector.rb index b4f164bff0..c8146a0ce7 100644 --- a/lib/open_project/content_type_detector.rb +++ b/lib/open_project/content_type_detector.rb @@ -91,15 +91,15 @@ module OpenProject # Returns a String describing the file's content type def detect - type = if blank_name? - SENSIBLE_DEFAULT - elsif empty_file? - EMPTY_TYPE - elsif calculated_type_matches.any? - calculated_type_matches.first - else - type_from_file_command || SENSIBLE_DEFAULT - end.to_s + if blank_name? + SENSIBLE_DEFAULT + elsif empty_file? + EMPTY_TYPE + elsif calculated_type_matches.any? + calculated_type_matches.first + else + type_from_file_command || SENSIBLE_DEFAULT + end.to_s end private From 9323b89bace87a89a5f488fb8a0106d855798b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Sun, 11 Jul 2021 22:07:53 +0200 Subject: [PATCH 47/68] Enforce writing the content type without accepting user input --- app/models/attachment.rb | 4 +--- app/services/attachments/set_attributes_service.rb | 6 ++++++ app/services/attachments/set_prepared_attributes_service.rb | 4 +++- app/workers/attachments/finish_direct_upload_job.rb | 6 +++--- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/models/attachment.rb b/app/models/attachment.rb index b70b90ede9..3ad5280628 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -175,9 +175,7 @@ class Attachment < ApplicationRecord end def set_content_type(file) - return if content_type.present? - - self.content_type = file.try(:content_type) || self.class.content_type_for(file.path) + self.content_type = self.class.content_type_for(file.path) end def set_digest(file) diff --git a/app/services/attachments/set_attributes_service.rb b/app/services/attachments/set_attributes_service.rb index 334ada1fac..3f7e5c5b52 100644 --- a/app/services/attachments/set_attributes_service.rb +++ b/app/services/attachments/set_attributes_service.rb @@ -30,6 +30,12 @@ module Attachments class SetAttributesService < ::BaseServices::SetAttributes + def set_attributes(params) + # Don't set the content type manually, + # we always want to infer it + super params.except :content_type + end + def set_default_attributes(params) model.author = user if model.author.nil? end diff --git a/app/services/attachments/set_prepared_attributes_service.rb b/app/services/attachments/set_prepared_attributes_service.rb index b97f25ac3f..f46d99334c 100644 --- a/app/services/attachments/set_prepared_attributes_service.rb +++ b/app/services/attachments/set_prepared_attributes_service.rb @@ -50,7 +50,9 @@ module Attachments model.extend(OpenProject::ChangedBySystem) model.change_by_system do model.downloads = -1 - model.content_type = params[:content_type] || 'application/octet-stream' + # Set a content type as the file is not present + # The real content type will be set by FinishDirectUploadJob + model.content_type = params[:content_type] || OpenProject::ContentTypeDetector::SENSIBLE_DEFAULT end end diff --git a/app/workers/attachments/finish_direct_upload_job.rb b/app/workers/attachments/finish_direct_upload_job.rb index f1893f00e5..0b319208d1 100644 --- a/app/workers/attachments/finish_direct_upload_job.rb +++ b/app/workers/attachments/finish_direct_upload_job.rb @@ -55,9 +55,9 @@ class Attachments::FinishDirectUploadJob < ApplicationJob def set_attributes_from_file(attachment, local_file) attachment.downloads = 0 - attachment.set_file_size local_file unless attachment.filesize && attachment.filesize > 0 - attachment.set_content_type local_file unless attachment.content_type.present? - attachment.set_digest local_file unless attachment.digest.present? + attachment.set_file_size local_file + attachment.set_content_type local_file + attachment.set_digest local_file end def save_attachment(attachment) From 69eb40e7a38b5a06801e90b356f569a3983ccaeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 12 Jul 2021 08:31:09 +0200 Subject: [PATCH 48/68] Expect changed content_type --- .../attachments/finish_direct_upload_job_integration_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/workers/attachments/finish_direct_upload_job_integration_spec.rb b/spec/workers/attachments/finish_direct_upload_job_integration_spec.rb index dc83bfc03c..ea16e4d309 100644 --- a/spec/workers/attachments/finish_direct_upload_job_integration_spec.rb +++ b/spec/workers/attachments/finish_direct_upload_job_integration_spec.rb @@ -48,8 +48,9 @@ describe Attachments::FinishDirectUploadJob, 'integration', type: :job do expect(attachment.downloads) .to eql(0) + # expect to replace the content type with the actual value expect(attachment.content_type) - .to eql('application/binary') + .to eql('text/plain') expect(attachment.digest) .to eql("9473fdd0d880a43c21b7778d34872157") end From 4d1efab3c669c574a4cd928d321274226067a492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 12 Jul 2021 09:04:19 +0200 Subject: [PATCH 49/68] Fix content of viewpoint image to get correct content type --- .../spec/factories/bcf_viewpoint_attachment_factory.rb | 10 ++++++---- .../spec/requests/api/bcf/v2_1/viewpoints_api_spec.rb | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/bim/spec/factories/bcf_viewpoint_attachment_factory.rb b/modules/bim/spec/factories/bcf_viewpoint_attachment_factory.rb index 84d091df05..31a583e0ff 100644 --- a/modules/bim/spec/factories/bcf_viewpoint_attachment_factory.rb +++ b/modules/bim/spec/factories/bcf_viewpoint_attachment_factory.rb @@ -29,13 +29,15 @@ FactoryBot.define do factory :bcf_viewpoint_attachment, class: 'Attachment' do description { "snapshot" } - filename { "snapshot.jpg" } + filename { "image.png" } content_type { "image/jpeg" } author { User.current } file do - OpenProject::Files.create_uploaded_file name: filename, - content_type: content_type, - binary: true + Rack::Test::UploadedFile.new( + Rails.root.join("spec/fixtures/files/image.png"), + 'image/png', + true, + ) end end end diff --git a/modules/bim/spec/requests/api/bcf/v2_1/viewpoints_api_spec.rb b/modules/bim/spec/requests/api/bcf/v2_1/viewpoints_api_spec.rb index 87aa1c53fa..b672b7dd5e 100644 --- a/modules/bim/spec/requests/api/bcf/v2_1/viewpoints_api_spec.rb +++ b/modules/bim/spec/requests/api/bcf/v2_1/viewpoints_api_spec.rb @@ -202,7 +202,7 @@ describe 'BCF 2.1 viewpoints resource', type: :request, content_type: :json, wit it 'responds with the attachment with the appropriate content type and cache headers' do expect(subject.status).to eq 200 - expect(subject.headers['Content-Type']).to eq 'image/jpeg' + expect(subject.headers['Content-Type']).to eq 'image/png' max_age = OpenProject::Configuration.fog_download_url_expires_in - 10 From b6237fcc889cdb185df80b9af1a2d292a1e3ddb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 12 Jul 2021 09:12:51 +0200 Subject: [PATCH 50/68] Fix tsv spec --- app/workers/extract_fulltext_job.rb | 2 +- spec/workers/extract_fulltext_job_spec.rb | 123 +++++++++++----------- 2 files changed, 64 insertions(+), 61 deletions(-) diff --git a/app/workers/extract_fulltext_job.rb b/app/workers/extract_fulltext_job.rb index 7fbbb7f093..975cac15bf 100644 --- a/app/workers/extract_fulltext_job.rb +++ b/app/workers/extract_fulltext_job.rb @@ -81,7 +81,7 @@ class ExtractFulltextJob < ApplicationJob end def find_attachment(id) - Attachment.find_by_id id + Attachment.find_by(id: id) end def remote_file? diff --git a/spec/workers/extract_fulltext_job_spec.rb b/spec/workers/extract_fulltext_job_spec.rb index 802b72b53a..1267ebaac8 100644 --- a/spec/workers/extract_fulltext_job_spec.rb +++ b/spec/workers/extract_fulltext_job_spec.rb @@ -31,86 +31,89 @@ require 'spec_helper' describe ExtractFulltextJob, type: :job do - # These jobs only get created when TSVector is supported by the DB. - if OpenProject::Database.allows_tsv? - let(:text) { 'lorem ipsum' } - let(:attachment) do - FactoryBot.create(:attachment).tap do |attachment| - expect(ExtractFulltextJob) - .to have_been_enqueued - .with(attachment.id) - - perform_enqueued_jobs - - attachment.reload - end + let(:text) { 'lorem ipsum' } + let(:attachment) do + FactoryBot.create(:attachment).tap do |attachment| + expect(ExtractFulltextJob) + .to have_been_enqueued + .with(attachment.id) end + end + + let(:subject) do + perform_enqueued_jobs + + attachment.reload + end + + before do + allow(Attachment).to receive(:find_by).with(id: attachment.id).and_return(attachment) + end - context "with successful text extraction" do + context "with successful text extraction" do + before do + allow_any_instance_of(Plaintext::Resolver).to receive(:text).and_return(text) + end + + context 'attachment is readable' do before do - allow_any_instance_of(Plaintext::Resolver).to receive(:text).and_return(text) + allow(attachment).to receive(:readable?).and_return(true) + subject end - context 'attachment is readable' do - before do - allow(attachment).to receive(:readable?).and_return(true) - attachment.reload - end - - it 'updates the attachment\'s DB record with fulltext, fulltext_tsv, and file_tsv' do - expect(attachment.fulltext).to eq text - expect(attachment.fulltext_tsv.size).to be > 0 - expect(attachment.file_tsv.size).to be > 0 - end + it 'updates the attachment\'s DB record with fulltext, fulltext_tsv, and file_tsv' do + expect(attachment.fulltext).to eq text + expect(attachment.fulltext_tsv.size).to be > 0 + expect(attachment.file_tsv.size).to be > 0 + end - # shared_examples 'no fulltext but file name saved as TSV' do - # end + # shared_examples 'no fulltext but file name saved as TSV' do + # end - context 'No content was extracted' do - let(:text) { nil } + context 'No content was extracted' do + let(:text) { nil } - # include_examples 'no fulltext but file name saved as TSV' - it 'updates the attachment\'s DB record with file_tsv' do - expect(attachment.fulltext).to be_blank - expect(attachment.fulltext_tsv).to be_blank - expect(attachment.file_tsv.size).to be > 0 - end + # include_examples 'no fulltext but file name saved as TSV' + it 'updates the attachment\'s DB record with file_tsv' do + expect(attachment.fulltext).to be_blank + expect(attachment.fulltext_tsv).to be_blank + expect(attachment.file_tsv.size).to be > 0 end end end + end - shared_examples 'only file name indexed' do - it 'updates the attachment\'s DB record with file_tsv' do - expect(attachment.fulltext).to be_blank - expect(attachment.fulltext_tsv).to be_blank - expect(attachment.file_tsv.size).to be > 0 - end + shared_examples 'only file name indexed' do + it 'updates the attachment\'s DB record with file_tsv' do + expect(attachment.fulltext).to be_blank + expect(attachment.fulltext_tsv).to be_blank + expect(attachment.file_tsv.size).to be > 0 end + end - context 'file not readable' do - before do - allow(attachment).to receive(:readable?).and_return(false) - attachment.reload - end - - include_examples 'only file name indexed' + context 'file not readable' do + before do + allow(attachment).to receive(:readable?).and_return(false) + subject end - context 'with exception in extraction' do - let(:exception_message) { 'boom-internal-error' } - let(:logger) { Rails.logger } + include_examples 'only file name indexed' + end - before do - allow_any_instance_of(Plaintext::Resolver).to receive(:text).and_raise(exception_message) + context 'with exception in extraction' do + let(:exception_message) { 'boom-internal-error' } + let(:logger) { Rails.logger } - # This line is actually part of the test. `expect` call needs to go so far up here, as we want to verify that a message gets logged. - expect(logger).to receive(:error).with(/boom-internal-error/) + before do + allow_any_instance_of(Plaintext::Resolver).to receive(:text).and_raise(exception_message) - allow(attachment).to receive(:readable?).and_return(true) - attachment.reload - end + # This line is actually part of the test. `expect` call needs to go so far up here, as we want to verify that a message gets logged. + expect(logger).to receive(:error).with(/boom-internal-error/) - include_examples 'only file name indexed' + allow(attachment).to receive(:readable?).and_return(true) + subject end + + include_examples 'only file name indexed' end end From 2d3a246fa4779eca08f16695cd45455f623ceeb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 12 Jul 2021 09:46:31 +0200 Subject: [PATCH 51/68] Add create contract spec --- app/contracts/attachments/create_contract.rb | 27 ++-- .../bcf_viewpoint_attachment_factory.rb | 2 +- .../attachments/create_contract_spec.rb | 143 ++++++++++++++++++ spec/factories/attachment_factory.rb | 4 + 4 files changed, 165 insertions(+), 11 deletions(-) create mode 100644 spec/contracts/attachments/create_contract_spec.rb diff --git a/app/contracts/attachments/create_contract.rb b/app/contracts/attachments/create_contract.rb index 60a7b57b13..6c7c36078c 100644 --- a/app/contracts/attachments/create_contract.rb +++ b/app/contracts/attachments/create_contract.rb @@ -72,20 +72,27 @@ module Attachments ## # Validates the content type, if a whitelist is set def validate_content_type - # In some cases, we do not want to process the whitelist - # for the attachment mime types. For example, when - # storing backups or exports - return unless options.fetch(:validate_content_type, true) - - whitelist = Array(Setting.attachment_whitelist) - # If the whitelist is empty, assume all files are allowed # as before - return if whitelist.empty? - - unless whitelist.include?(model.content_type) || whitelist.include?("*#{model.extension}") + unless matches_whitelist?(attachment_whitelist) + Rails.logger.info { "Uploaded file #{model.filename} with type #{model.content_type} does not match whitelist" } errors.add :content_type, :not_whitelisted, value: model.content_type end end + + ## + # Get the user-defined whitelist or a custom whitelist + # defined for this invocation + def attachment_whitelist + Array(options.fetch(:whitelist, Setting.attachment_whitelist)) + end + + ## + # Returns whether the attachment matches the whitelist + def matches_whitelist?(whitelist) + return true if whitelist.empty? + + whitelist.include?(model.content_type) || whitelist.include?("*#{model.extension}") + end end end diff --git a/modules/bim/spec/factories/bcf_viewpoint_attachment_factory.rb b/modules/bim/spec/factories/bcf_viewpoint_attachment_factory.rb index 31a583e0ff..8f88d96dec 100644 --- a/modules/bim/spec/factories/bcf_viewpoint_attachment_factory.rb +++ b/modules/bim/spec/factories/bcf_viewpoint_attachment_factory.rb @@ -36,7 +36,7 @@ FactoryBot.define do Rack::Test::UploadedFile.new( Rails.root.join("spec/fixtures/files/image.png"), 'image/png', - true, + true ) end end diff --git a/spec/contracts/attachments/create_contract_spec.rb b/spec/contracts/attachments/create_contract_spec.rb new file mode 100644 index 0000000000..efcce412ac --- /dev/null +++ b/spec/contracts/attachments/create_contract_spec.rb @@ -0,0 +1,143 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require 'contracts/shared/model_contract_shared_context' + +describe Attachments::CreateContract do + include_context 'ModelContract shared context' + + let(:current_user) { FactoryBot.build_stubbed :user } + let(:model) do + FactoryBot.build :attachment, + container: container, + content_type: content_type, + file: file, + filename: filename, + author: current_user + end + let(:contract) { described_class.new model, user, options: contract_options } + let(:contract_options) { {} } + + let(:user) { current_user } + let(:container) { nil } + let(:file) do + Rack::Test::UploadedFile.new( + Rails.root.join("spec/fixtures/files/image.png"), + 'image/png', + true + ) + end + let(:content_type) { 'image/png' } + let(:filename) { 'image.png' } + + let(:can_attach_global) { true } + + before do + allow(Redmine::Acts::Attachable.attachables) + .to receive(:none?).and_return(!can_attach_global) + end + + context 'with user who has no permissions' do + let(:can_attach_global) { false } + + it_behaves_like 'contract is invalid', base: :error_unauthorized + end + + context 'with a user that is not the author' do + let(:user) { FactoryBot.build_stubbed :user } + + it_behaves_like 'contract is invalid', author: :invalid + end + + context 'with user who has permissions to add' do + it_behaves_like 'contract is valid' + end + + context 'with a container' do + let(:container) { FactoryBot.build_stubbed :work_package } + + before do + allow(container) + .to receive(:attachments_addable?) + .with(user) + .and_return(can_attach) + end + + context 'with user who has no permissions' do + let(:can_attach) { false } + + it_behaves_like 'contract is invalid', base: :error_unauthorized + end + + context 'with user who has permissions to add' do + let(:can_attach) { true } + + it_behaves_like 'contract is valid' + end + + end + + context 'with an empty whitelist', + with_settings: { attachment_whitelist: %w[] } do + it_behaves_like 'contract is valid' + end + + context 'with a matching mime whitelist', + with_settings: { attachment_whitelist: %w[image/png] } do + it_behaves_like 'contract is valid' + end + + context 'with a matching extension whitelist', + with_settings: { attachment_whitelist: %w[*.png] } do + it_behaves_like 'contract is valid' + end + + context 'with a non-matching whitelist', + with_settings: { attachment_whitelist: %w[*.jpg image/jpeg] } do + it_behaves_like 'contract is invalid', content_type: :not_whitelisted + + context 'when disabling the whitelist check' do + let(:contract_options) do + { whitelist: [] } + end + + it_behaves_like 'contract is valid' + end + + context 'when overriding the whitelist' do + let(:contract_options) do + { whitelist: %w[*.png] } + end + + it_behaves_like 'contract is valid' + end + end +end diff --git a/spec/factories/attachment_factory.rb b/spec/factories/attachment_factory.rb index e09b57ab4a..55c37949cb 100644 --- a/spec/factories/attachment_factory.rb +++ b/spec/factories/attachment_factory.rb @@ -45,6 +45,10 @@ FactoryBot.define do binary: true end + callback(:after_build, :after_stub) do |attachment, evaluator| + attachment.filename = evaluator.filename if evaluator.filename + end + factory :wiki_attachment do container factory: :wiki_page_with_content end From 15fc0846e6cac22f8e88fda953222ea6e9cf4f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 12 Jul 2021 09:57:24 +0200 Subject: [PATCH 52/68] Bypass whitelist in internal services when conflicting with user --- app/services/attachments/create_service.rb | 12 ++++++++++++ app/workers/backup_job.rb | 2 +- app/workers/work_packages/exports/export_job.rb | 2 +- .../bim/app/controllers/bim/bcf/issues_controller.rb | 2 +- modules/bim/app/models/bim/ifc_models/ifc_model.rb | 2 +- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/services/attachments/create_service.rb b/app/services/attachments/create_service.rb index 33f00f1586..850f6c13e0 100644 --- a/app/services/attachments/create_service.rb +++ b/app/services/attachments/create_service.rb @@ -31,6 +31,18 @@ class Attachments::CreateService < ::BaseServices::Create around_call :error_wrapped_call + ## + # Create an attachment service bypassing the user-provided whitelist + # for internal purposes such as exporting data. + # + # @param user The user to call the service with + # @param whitelist A custom whitelist to validate with, or empty to disable validation + # + # Warning: When passing an empty whitelist, this results in no validations on the content type taking place. + def self.bypass_whitelist(user:, whitelist: []) + new(user: user, contract_options: { whitelist: whitelist.map(&:to_s) }) + end + def persist(call) attachment = call.result if attachment.container diff --git a/app/workers/backup_job.rb b/app/workers/backup_job.rb index 964d5229ee..9d95183339 100644 --- a/app/workers/backup_job.rb +++ b/app/workers/backup_job.rb @@ -121,7 +121,7 @@ class BackupJob < ::ApplicationJob def store_backup(file_name, backup:, user:) File.open(file_name) do |file| call = Attachments::CreateService - .new(user: user) + .bypass_whitelist(user: user) .call(container: backup, filename: file_name, file: file, description: 'OpenProject backup') call.on_success do diff --git a/app/workers/work_packages/exports/export_job.rb b/app/workers/work_packages/exports/export_job.rb index c0a4544b05..ea09c48680 100644 --- a/app/workers/work_packages/exports/export_job.rb +++ b/app/workers/work_packages/exports/export_job.rb @@ -90,7 +90,7 @@ module WorkPackages def store_attachment(container, file) call = Attachments::CreateService - .new(user: User.current) + .bypass_whitelist(user: User.current) .call(container: container, file: file, filename: File.basename(file), description: '') call.on_success do diff --git a/modules/bim/app/controllers/bim/bcf/issues_controller.rb b/modules/bim/app/controllers/bim/bcf/issues_controller.rb index 6c5a33097e..3d3269dd64 100644 --- a/modules/bim/app/controllers/bim/bcf/issues_controller.rb +++ b/modules/bim/app/controllers/bim/bcf/issues_controller.rb @@ -215,7 +215,7 @@ module Bim def create_attachment filename = params[:bcf_file].original_filename call = Attachments::CreateService - .new(user: current_user) + .bypass_whitelist(user: current_user, whitelist: %w[application/zip]) .call(file: params[:bcf_file], filename: filename, description: filename) diff --git a/modules/bim/app/models/bim/ifc_models/ifc_model.rb b/modules/bim/app/models/bim/ifc_models/ifc_model.rb index 807606a8bd..940acb76fe 100644 --- a/modules/bim/app/models/bim/ifc_models/ifc_model.rb +++ b/modules/bim/app/models/bim/ifc_models/ifc_model.rb @@ -26,7 +26,7 @@ module Bim delete_attachment name call = ::Attachments::CreateService - .new(user: User.current) + .bypass_whitelist(user: User.current) .call(file: file, container: self, filename: file.original_filename, description: name) call.on_failure { Rails.logger.error "Failed to add #{name} attachment: #{call.message}" } From b3ab397c94a529cedf3ce12ff614101eb78eef0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 12 Jul 2021 10:18:11 +0200 Subject: [PATCH 53/68] Fix expects in specs after whitelist bypass --- modules/bim/spec/bcf/bcf_xml/issue_writer_spec.rb | 2 +- modules/bim/spec/controllers/issues_controller_spec.rb | 4 ++-- .../bim/spec/workers/work_packages/exports/export_job_spec.rb | 2 +- spec/workers/work_packages/exports/export_job_spec.rb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/bim/spec/bcf/bcf_xml/issue_writer_spec.rb b/modules/bim/spec/bcf/bcf_xml/issue_writer_spec.rb index f23db8f793..2c3b43f10b 100644 --- a/modules/bim/spec/bcf/bcf_xml/issue_writer_spec.rb +++ b/modules/bim/spec/bcf/bcf_xml/issue_writer_spec.rb @@ -183,7 +183,7 @@ describe ::OpenProject::Bim::BcfXml::IssueWriter do uuid = bcf_issue.viewpoints.first.uuid viewpoint_node = subject.at("/Markup/Viewpoints[@Guid='#{uuid}']") expect(viewpoint_node.at('Viewpoint').content).to eql("#{uuid}.xml") - expect(viewpoint_node.at('Snapshot').content).to eql("#{uuid}.jpg") + expect(viewpoint_node.at('Snapshot').content).to eql("#{uuid}.png") end end diff --git a/modules/bim/spec/controllers/issues_controller_spec.rb b/modules/bim/spec/controllers/issues_controller_spec.rb index 08eb3e451a..6a24ad6804 100644 --- a/modules/bim/spec/controllers/issues_controller_spec.rb +++ b/modules/bim/spec/controllers/issues_controller_spec.rb @@ -103,7 +103,7 @@ describe ::Bim::Bcf::IssuesController, type: :controller do let(:file) do Rack::Test::UploadedFile.new( File.join(Rails.root, "modules/bim/spec/fixtures/files/#{filename}"), - 'application/octet-stream' + 'application/zip' ) end @@ -119,7 +119,7 @@ describe ::Bim::Bcf::IssuesController, type: :controller do let(:file) { FileHelpers.mock_uploaded_file } it 'should redirect back to where we started from' do - expect { action }.to change { Attachment.count }.by(1) + expect { action }.to change { Attachment.count }.by(0) expect(response).to redirect_to '/projects/bim_project/issues/upload' end end diff --git a/modules/bim/spec/workers/work_packages/exports/export_job_spec.rb b/modules/bim/spec/workers/work_packages/exports/export_job_spec.rb index 2e4b87d2f7..bd0dc39871 100644 --- a/modules/bim/spec/workers/work_packages/exports/export_job_spec.rb +++ b/modules/bim/spec/workers/work_packages/exports/export_job_spec.rb @@ -67,7 +67,7 @@ describe WorkPackages::Exports::ExportJob do service = double('attachments create service') expect(Attachments::CreateService) - .to receive(:new) + .to receive(:bypass_whitelist) .with(user: user) .and_return(service) diff --git a/spec/workers/work_packages/exports/export_job_spec.rb b/spec/workers/work_packages/exports/export_job_spec.rb index 99d2d92f86..48ed7ffd6c 100644 --- a/spec/workers/work_packages/exports/export_job_spec.rb +++ b/spec/workers/work_packages/exports/export_job_spec.rb @@ -68,7 +68,7 @@ describe WorkPackages::Exports::ExportJob do service = double('attachments create service') expect(Attachments::CreateService) - .to receive(:new) + .to receive(:bypass_whitelist) .with(user: user) .and_return(service) From 992ef042ef7f6c058738cd1a02a742af9a5898a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 14 Jul 2021 13:51:07 +0200 Subject: [PATCH 54/68] Render contract errors for wiki --- app/controllers/wiki_controller.rb | 2 ++ app/views/wiki/_page_form.html.erb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index b003b9d9be..a9b3c37ac8 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -117,6 +117,7 @@ class WikiController < ApplicationController flash[:notice] = I18n.t(:notice_successful_create) redirect_to_show else + @errors = call.errors render action: 'new' end end @@ -180,6 +181,7 @@ class WikiController < ApplicationController flash[:notice] = I18n.t(:notice_successful_update) redirect_to_show else + @errors = call.errors render action: 'edit' end rescue ActiveRecord::StaleObjectError diff --git a/app/views/wiki/_page_form.html.erb b/app/views/wiki/_page_form.html.erb index 41b39cd47b..d5f8c8c369 100644 --- a/app/views/wiki/_page_form.html.erb +++ b/app/views/wiki/_page_form.html.erb @@ -1,4 +1,4 @@ -<%= error_messages_for 'page' %> +<%= error_messages_for_contract @page, @errors %> <% resource = ::API::V3::WikiPages::WikiPageRepresenter.new(@page, current_user: current_user, embed_links: true) %> From 07be595f4fe58ef491406e2d1809f9c4517e4633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 14 Jul 2021 13:56:29 +0200 Subject: [PATCH 55/68] Add before_hook to bodied to allow to pre-authorize permissions --- lib/api/utilities/endpoints/bodied.rb | 6 +- .../attachments_by_container_api.rb | 60 +++++++++---------- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/lib/api/utilities/endpoints/bodied.rb b/lib/api/utilities/endpoints/bodied.rb index 86ed88884e..6e207559ea 100644 --- a/lib/api/utilities/endpoints/bodied.rb +++ b/lib/api/utilities/endpoints/bodied.rb @@ -58,6 +58,7 @@ module API params_modifier: default_params_modifier, params_source: default_params_source, process_state: default_process_state, + before_hook: nil, parse_representer: nil, render_representer: nil, process_service: nil, @@ -74,12 +75,14 @@ module API self.process_contract = process_contract || deduce_process_contract self.process_service = process_service || deduce_process_service self.parse_service = parse_service || deduce_parse_service + self.before_hook = before_hook end def mount update = self -> do + update.before_hook&.(request: self) params = update.parse(self) call = update.process(self, params) @@ -134,7 +137,8 @@ module API :process_contract, :process_service, :parse_service, - :process_state + :process_state, + :before_hook private diff --git a/lib/api/v3/attachments/attachments_by_container_api.rb b/lib/api/v3/attachments/attachments_by_container_api.rb index d2babf01d5..bb506529d9 100644 --- a/lib/api/v3/attachments/attachments_by_container_api.rb +++ b/lib/api/v3/attachments/attachments_by_container_api.rb @@ -53,14 +53,14 @@ module API def restrict_permissions(permissions) authorize_any(permissions, projects: container.project) unless permissions.empty? end + end - def parse_multipart(request) - request.params.tap do |params| - params[:metadata] = JSON.parse(params[:metadata]) if params.key?(:metadata) - end - rescue JSON::ParserError - raise ::API::Errors::InvalidRequestBody.new(I18n.t('api_v3.errors.invalid_json')) + def self.parse_multipart(request) + request.params.tap do |params| + params[:metadata] = JSON.parse(params[:metadata]) if params.key?(:metadata) end + rescue JSON::ParserError + raise ::API::Errors::InvalidRequestBody.new(I18n.t('api_v3.errors.invalid_json')) end def self.read @@ -73,36 +73,30 @@ module API end def self.create(permissions = []) - -> do - restrict_permissions permissions - - instance_exec &::API::V3::Utilities::Endpoints::Create - .new(model: ::Attachment, - parse_representer: AttachmentParsingRepresenter, - params_source: method(:parse_multipart), - params_modifier: ->(params) do - params.merge(container: container) - end) - .mount - end + ::API::V3::Utilities::Endpoints::Create + .new(model: ::Attachment, + parse_representer: AttachmentParsingRepresenter, + params_source: method(:parse_multipart), + before_hook: ->(request:) { request.restrict_permissions(permissions) }, + params_modifier: ->(params) do + params.merge(container: container) + end) + .mount end def self.prepare(permissions = []) - -> do - restrict_permissions permissions - - instance_exec &::API::V3::Utilities::Endpoints::Create - .new(model: ::Attachment, - parse_representer: AttachmentParsingRepresenter, - render_representer: AttachmentUploadRepresenter, - process_service: ::Attachments::PrepareUploadService, - process_contract: ::Attachments::PrepareUploadContract, - params_source: method(:parse_multipart), - params_modifier: ->(params) do - params.merge(container: container) - end) - .mount - end + ::API::V3::Utilities::Endpoints::Create + .new(model: ::Attachment, + parse_representer: AttachmentParsingRepresenter, + render_representer: AttachmentUploadRepresenter, + process_service: ::Attachments::PrepareUploadService, + process_contract: ::Attachments::PrepareUploadContract, + params_source: method(:parse_multipart), + before_hook: ->(request:) { request.restrict_permissions(permissions) }, + params_modifier: ->(params) do + params.merge(container: container) + end) + .mount end end end From 26b8a8a1d4ce97e6cc504d726802471549fc8f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 14 Jul 2021 14:00:35 +0200 Subject: [PATCH 56/68] Budget errors from contract --- modules/budgets/app/controllers/budgets_controller.rb | 2 ++ modules/budgets/app/views/budgets/_edit.html.erb | 1 - modules/budgets/app/views/budgets/_form.html.erb | 1 + modules/budgets/app/views/budgets/new.html.erb | 1 - 4 files changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/budgets/app/controllers/budgets_controller.rb b/modules/budgets/app/controllers/budgets_controller.rb index 7127ba4ced..370428f6f9 100644 --- a/modules/budgets/app/controllers/budgets_controller.rb +++ b/modules/budgets/app/controllers/budgets_controller.rb @@ -108,6 +108,7 @@ class BudgetsController < ApplicationController flash[:notice] = t(:notice_successful_create) redirect_to(params[:continue] ? { action: 'new' } : { action: 'show', id: @budget }) else + @errors = call.errors render action: 'new', layout: !request.xhr? end end @@ -126,6 +127,7 @@ class BudgetsController < ApplicationController redirect_to(params[:back_to] || { action: 'show', id: @budget }) else @budget = call.result + @errors = call.errors render action: 'edit' end rescue ActiveRecord::StaleObjectError diff --git a/modules/budgets/app/views/budgets/_edit.html.erb b/modules/budgets/app/views/budgets/_edit.html.erb index 4a392b1517..9e195f8027 100644 --- a/modules/budgets/app/views/budgets/_edit.html.erb +++ b/modules/budgets/app/views/budgets/_edit.html.erb @@ -33,7 +33,6 @@ See docs/COPYRIGHT.rdoc for more details. :html => {:multipart => true, :id => 'budget_form', :class => 'form'} do |f| %> - <%= error_messages_for 'budget' %> <%= render :partial => 'form', :locals => {:f => f} %>
<%= styled_button_tag t(:button_submit), class: '-with-icon icon-checkmark -highlight', id: 'budget-table--submit-button'%> diff --git a/modules/budgets/app/views/budgets/_form.html.erb b/modules/budgets/app/views/budgets/_form.html.erb index e3162429f0..85339408ac 100644 --- a/modules/budgets/app/views/budgets/_form.html.erb +++ b/modules/budgets/app/views/budgets/_form.html.erb @@ -28,6 +28,7 @@ See docs/COPYRIGHT.rdoc for more details. ++#%> <% resource = budget_attachment_representer(f.object) %> +<%= error_messages_for_contract @budget, @errors %>
<%= f.text_field :subject, required: true, autofocus: true, container_class: '-wide' %> diff --git a/modules/budgets/app/views/budgets/new.html.erb b/modules/budgets/app/views/budgets/new.html.erb index 5e8729f9f5..3f872e89f7 100644 --- a/modules/budgets/app/views/budgets/new.html.erb +++ b/modules/budgets/app/views/budgets/new.html.erb @@ -34,7 +34,6 @@ See docs/COPYRIGHT.rdoc for more details. :url => projects_budgets_path(@project), :as => :budget, :html => {:multipart => true, :id => 'budget_form'} do |f| %> - <%= error_messages_for 'budget' %> <%= render :partial => 'form', :locals => {:f => f} %> <%= styled_button_tag t(:button_create), class: '-with-icon icon-checkmark' %> <%= styled_button_tag t(:button_create_and_continue), :name => 'continue', From 7502cd777d610634f5ea46d605d1be195fe23d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 14 Jul 2021 14:01:15 +0200 Subject: [PATCH 57/68] Document errors from contract --- config/settings.yml | 2 ++ modules/documents/app/controllers/documents_controller.rb | 2 ++ modules/documents/app/views/documents/_form.html.erb | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/config/settings.yml b/config/settings.yml index c5fcedbf9e..41bd744250 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -326,6 +326,8 @@ session_ttl_enabled: session_ttl: format: int default: 120 +default_notification_option: + default: 'only_my_events' emails_header: serialized: true default: diff --git a/modules/documents/app/controllers/documents_controller.rb b/modules/documents/app/controllers/documents_controller.rb index 86b58661f7..1c3f4a2249 100644 --- a/modules/documents/app/controllers/documents_controller.rb +++ b/modules/documents/app/controllers/documents_controller.rb @@ -73,6 +73,7 @@ class DocumentsController < ApplicationController redirect_to project_documents_path(@project) else @document = call.result + @errors = call.errors render action: 'new' end end @@ -91,6 +92,7 @@ class DocumentsController < ApplicationController redirect_to action: 'show', id: @document else @document = call.result + @errors = call.errors render action: 'edit' end end diff --git a/modules/documents/app/views/documents/_form.html.erb b/modules/documents/app/views/documents/_form.html.erb index 26cb35da22..3957fc46b5 100644 --- a/modules/documents/app/views/documents/_form.html.erb +++ b/modules/documents/app/views/documents/_form.html.erb @@ -27,7 +27,7 @@ See docs/COPYRIGHT.rdoc for more details. ++#%> -<%= error_messages_for 'document' %> +<%= error_messages_for_contract @document, @errors %>
<%= f.select :category_id, From e562d612b703eacc75e2cfa170679f4c379a4c4f Mon Sep 17 00:00:00 2001 From: Markus Kahl Date: Mon, 20 Sep 2021 13:19:40 +0100 Subject: [PATCH 58/68] ee token reprieve support --- Gemfile | 2 +- app/models/enterprise_token.rb | 6 +- app/views/enterprises/_current.html.erb | 7 +- config/locales/js-en.yml | 2 + .../ee-active-saved-trial.component.ts | 12 +++ .../ee-active-trial.base.ts | 4 +- .../ee-active-trial.component.html | 1 + .../ee-active-trial.component.sass | 6 +- ...d_with_7_days_reprieve_at_2021_09_01.token | 42 +++++++++ ...pired_without_reprieve_at_2021_09_01.token | 42 +++++++++ .../enterprises/_current.html.erb_spec.rb | 86 +++++++++++++++++++ 11 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 spec/fixtures/ee_tokens/v1_expired_with_7_days_reprieve_at_2021_09_01.token create mode 100644 spec/fixtures/ee_tokens/v1_expired_without_reprieve_at_2021_09_01.token create mode 100644 spec/views/admin/enterprises/_current.html.erb_spec.rb diff --git a/Gemfile b/Gemfile index 09129acf66..8811e8c279 100644 --- a/Gemfile +++ b/Gemfile @@ -180,7 +180,7 @@ gem 'aws-sdk-core', '~> 3.107' # File upload via fog + screenshots on travis gem 'aws-sdk-s3', '~> 1.91' -gem 'openproject-token', '~> 2.1.1' +gem 'openproject-token', '~> 2.2.0' gem 'plaintext', '~> 0.3.2' diff --git a/app/models/enterprise_token.rb b/app/models/enterprise_token.rb index 09b4ece4b9..f5f795dcb6 100644 --- a/app/models/enterprise_token.rb +++ b/app/models/enterprise_token.rb @@ -69,6 +69,8 @@ class EnterpriseToken < ApplicationRecord :issued_at, :starts_at, :expires_at, + :reprieve_days, + :reprieve_days_left, :restrictions, to: :token_object @@ -86,8 +88,8 @@ class EnterpriseToken < ApplicationRecord RequestStore.delete :current_ee_token end - def expired? - token_object.expired? || invalid_domain? + def expired?(reprieve: true) + token_object.expired?(reprieve: reprieve) || invalid_domain? end ## diff --git a/app/views/enterprises/_current.html.erb b/app/views/enterprises/_current.html.erb index dfc7693b3e..34f3e7693f 100644 --- a/app/views/enterprises/_current.html.erb +++ b/app/views/enterprises/_current.html.erb @@ -5,10 +5,13 @@ data-domain="<%= @current_token.try(:domain) %>" data-user-count="<%= @current_token.restrictions.nil? ? t('js.admin.enterprise.upsale.unlimited') : @current_token.restrictions[:active_user_count] %>" data-starts-at="<%= format_date @current_token.starts_at %>" - data-expires-at="<%= (!@current_token.will_expire?) ? t('js.admin.enterprise.upsale.unlimited') : (format_date @current_token.expires_at) %>"> + data-expires-at="<%= (!@current_token.will_expire?) ? t('js.admin.enterprise.upsale.unlimited') : (format_date @current_token.expires_at) %>" + data-is-expired="<%= @current_token.expired?(reprieve: false) %>" + data-reprieve-days-left="<%= @current_token.reprieve_days_left %>" +> -<%= form_tag({}, method: :delete) do %> +<%= form_tag(enterprise_path, method: :delete) do %> <%= styled_button_tag t(:button_delete), method: :delete, diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index b331fffce5..ea06023abb 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -209,6 +209,8 @@ en: new_group: 'New group' reset_to_defaults: 'Reset to defaults' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation of email address" confirmation_info: > diff --git a/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-saved-trial.component.ts b/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-saved-trial.component.ts index a5de4c050f..3ee6acd3b9 100644 --- a/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-saved-trial.component.ts +++ b/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-saved-trial.component.ts @@ -45,9 +45,21 @@ export class EEActiveSavedTrialComponent extends EEActiveTrialBase { public userCount = this.elementRef.nativeElement.dataset['userCount']; public startsAt = this.elementRef.nativeElement.dataset['startsAt']; public expiresAt = this.elementRef.nativeElement.dataset['expiresAt']; + public isExpired:boolean = this.elementRef.nativeElement.dataset['isExpired'] == 'true'; + public reprieveDaysLeft = this.elementRef.nativeElement.dataset['reprieveDaysLeft']; constructor(readonly elementRef:ElementRef, readonly I18n:I18nService) { super(I18n); } + + public get expiredWarningText():string { + var warning = this.text.text_expired; + + if (this.reprieveDaysLeft && this.reprieveDaysLeft > 0) { + warning = warning + ": " + this.text.text_reprieve_days_left(this.reprieveDaysLeft); + } + + return warning; + } } diff --git a/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.base.ts b/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.base.ts index 6d6460c74f..6006745c2d 100644 --- a/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.base.ts +++ b/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.base.ts @@ -37,7 +37,9 @@ export class EEActiveTrialBase extends UntilDestroyedMixin { label_company: this.I18n.t('js.admin.enterprise.trial.form.label_company'), label_domain: this.I18n.t('js.admin.enterprise.trial.form.label_domain'), label_starts_at: this.I18n.t('js.admin.enterprise.trial.form.label_starts_at'), - label_subscriber: this.I18n.t('js.admin.enterprise.trial.form.label_subscriber') + label_subscriber: this.I18n.t('js.admin.enterprise.trial.form.label_subscriber'), + text_expired: this.I18n.t('js.admin.enterprise.text_expired'), + text_reprieve_days_left: (days:string) => this.I18n.t('js.admin.enterprise.text_reprieve_days_left', { days: days }) }; constructor(readonly I18n:I18nService) { diff --git a/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.component.html b/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.component.html index 9ed71ef084..484a1ea482 100644 --- a/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.component.html +++ b/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.component.html @@ -42,6 +42,7 @@
+
diff --git a/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.component.sass b/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.component.sass index 7d3f0c12d0..19f79dd2ae 100644 --- a/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.component.sass +++ b/frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.component.sass @@ -1,2 +1,6 @@ .attributes-group - margin-bottom: 1.25rem \ No newline at end of file + margin-bottom: 1.25rem +.expired-warning + margin-left: 1rem + color: red + font-weight: bold diff --git a/spec/fixtures/ee_tokens/v1_expired_with_7_days_reprieve_at_2021_09_01.token b/spec/fixtures/ee_tokens/v1_expired_with_7_days_reprieve_at_2021_09_01.token new file mode 100644 index 0000000000..0ca597fe23 --- /dev/null +++ b/spec/fixtures/ee_tokens/v1_expired_with_7_days_reprieve_at_2021_09_01.token @@ -0,0 +1,42 @@ +-----BEGIN OPENPROJECT-EE TOKEN----- +eyJkYXRhIjoiMjhjK0xKa2wzS3NrUkVOdXlIWmZBMEg5bHRnbjRIQmxCV0NI +SFBZeDF5YXhvL01qT25Ud1JxUUo1bzlPXG5FaUN0MlRYTzJGNE1PdXhXdUtD +MHo5ek5xZFVHUTlFM3hrbGJGdHpSWldKbmZCTnd4WGIvSzY3NFhDZTlcbmhY +ZmhCcmJlVnFDOVV2Vm5jSExaelVwWVc2ZHpEbmdpRUorV2JlWFdJOEFwbFZj +VWxYZTRZUlRxREl0S1xuYUc4NHVjMDM0SmVHQjNyMS9hQnhnVVhrcFZoZXEy +aHR2VEJ6NWdNRkRBMExjMVNDL2E0K3Q0TzlMZmh6XG5vODJLdUF3MitGNlR2 +bG5nbE1JYzdLTm9OdmZFYWkzT0ZXc2FjVHFTcEk3NjJvVlJGbVU5aS9Ramhn +NXVcblI1bkdkY3ZMdjM0N1VlemY0OWdaXG4iLCJrZXkiOiJlWFhCZll2QjV4 +cmVBSEg1Lyt5RVlWaDZCV2lmRU1DdnRlc2xMdWIzQlNQd3BKNkZCc1YzbEhy +bS81WUlcbm9nZUJYSkVwMXY5SnU5K1Nvck43UW9OMWxQS3F3TWRsTGpmSlY0 +VHZneUVNWng5Sk1wTVdndXVXTkI1blxuT004RXhBd1dxL3ArUTF2OExYem5t +ZTZDK05CMmh6c0ovVHovL2JSazBGaVNaWnFDcEFXNmtwZDZ2M2tVXG5nODJT +SmdHOWNKd3Z5djE2VHJPc1owM2lzRE0xR3pmWmZIMVZKa00waHhLTFJpUjZO +Z25qUXBzbGhWc2dcbmxmVFNiZjhPR3N3NHBpUTZZRXh1ZkJTZ1RQcWpPc0Qr +dU9FYUYzTzB0NzYrZ3dJUnZDMWh4WWJibDBPUFxuRkwwaGI2WG13RjVIUE8x +KzZrSlNCZCtQMWViK3RiUDJjMDZCKzZmZ04rckZNSEFtd1NKMHBtdEVhRjRC +XG5CbEM1Z3JmbE9lemhpbkRBdXFIWFVyMk1mdjIzUHpNMjhXU3V2cGZsTUcz +clhyL2JJZjAxY3Rsc3lQRWJcbnlha1h4bVJRaTNLeWExT21PbWthV3BpVjEy +OEpiREpvN2g3cjBkZ0RoRDRFcExGNm9RaWxmYnA2MUdDQlxuQ1JqYm81cFNJ +Y0RUV1Z6SGZDN25jQmgvQW5vSDE2cTRMMzVqODB0VXBQVXBxblp6b0J4ZDBn +NTQrWHNVXG5WU2hOZngxMTNzSXR3dWFKREg0Wm9sSGx0clZXbXUwNncvL3ZQ +TUJHQ1lCdXc3OEkvNTNDV2IxeHZINUxcbkt4dmxoaVhrSVhqdmg5L3FrdWJJ +T2FWUUNmbG1sYW02YnJMMDEwVE5mNGdlU2VXd0QzcTZkU09aVUZTNVxuSkpR +UlpQQ0U3L3haZ0FmR2pzOERZeE5oVHp5Y1ZmbkhDR01VbnNOeThYcWJ1QUxV +aFhHODRGenBmbDF3XG5Rb0JIdEREaTVXbVJPdTQxNjh0US9iYXhBUEZHUjdK +UUpuYTBIdE9qV0VwU2h5b21Ra3g4bWIxY01hdDhcbk5MN0dBM0xUYnRMUm05 +UXVyTkpmblo0dkFhM1czb2pEaEpwZmFmcktDTjVQS1NTMmdNeWQyTEdpaEhr +VVxuSWhYQUJCSzBjWS9rYUlMZnA2SHcvdkk3SWczZ3NYNWRPQlFQQlY1SHJ6 +T255eXFwVzZwb0ZPRkw0STNyXG5lV1pDWUd6ZEY2UmUxS0RKRVVSeVlSODdV +YVYreXpuVWFtaWV5ZHpoc0g5SUN4cVlNamt1NHdXYkVqdmZcbm9FY3ZGYi9T +WHEwVHBEemd5cDNHNm9tRFNuUFg2b3kwL2Ivb2ZzSHhrRXRVYS9NK3czWTdi +QTI0TUJUQ1xuTi8wZWRxV0s5b0IzWjdKaTBQVEU0cmk0R09ZckppNXdHRENw +WTlaN1JscGhmS2gzeC9leFZiTkFsUENYXG5DeEhxbUs2cXZLbG5Zei9QYVpa +dWFKZGlsTFJYM0luNno0NFBUWDBGTVNRNTFqcWwzNlZJWnZEUlNaTkJcbk05 +Tnl3b2JJUkp3VncweVVLbDFhT0V4ODYzRjVKMThKK0VHa2dkMHR0YnZsNjRS +Vm1aSTZPM2lqdTdDMVxuanpIbzlDZjJRQi9uVk9SNldLTytWczZTTE5FcHpx +dGlHbU5jWUpPM3Mybit0QlUrL2xPMlBmbDQ0OVlCXG5rcGVBTUYvM0RERTVL +YzI3ZndpY1NENDFBWWl0c0llN01Eb1pUMG5wOXlVM2dxQUthMkhQd2dnMWcw +R0xcbjBGVEtoWkkwendTQkhwZU5rZzM5Q0VZUDVWQk8vdz09XG4iLCJpdiI6 +IkREbjZ3cm1NczBGaFk5cVFtanNuQXc9PVxuIn0= +-----END OPENPROJECT-EE TOKEN----- diff --git a/spec/fixtures/ee_tokens/v1_expired_without_reprieve_at_2021_09_01.token b/spec/fixtures/ee_tokens/v1_expired_without_reprieve_at_2021_09_01.token new file mode 100644 index 0000000000..4674d8ec34 --- /dev/null +++ b/spec/fixtures/ee_tokens/v1_expired_without_reprieve_at_2021_09_01.token @@ -0,0 +1,42 @@ +-----BEGIN OPENPROJECT-EE TOKEN----- +eyJkYXRhIjoiR0hXTnJxS0VhZUhVeWozcks0elh4K3dTeEpMMGhPbVd0VVRK +cXpmY3UzUWFLcGZDQVd2Vmxhc2JEQ0F0XG5lQkZ5U0JiSUs5VkMyQXkzaFBD +VHl5dy9ibFY5ZXhaaGRqTjVyd1dVbUxpTHJ3czZqRkVtZnNmemJCUURcbkgv +aWtUSm5rRGU1RkRqeDBrR29iTmJSbVg5K1hjc2xYZjNFSEN5ZDV2TUpjd1dF +UDFQQW5WaUJmNWdEL1xuMDlLWW9BT1NBVDI5cDJUclVOb25ic0NqMjFJa1Vy +dUxFdGhSa2dpa2N2dHVXcldoQWpuWC9qUWNsOXoxXG5tRzlvVWJOUzU4dWN6 +VHdSNysxU25SQ1dEK0tKNElUSG15bm5vODFQWVkxT21yckdiYjY5azZrSlFC +MkRcbmNqZTkvRkF3T1doSGxyMlRtNUxvXG4iLCJrZXkiOiJOU1NHYkR6aHoy +ekl6SkkreWFwdituaEhCREw0WmxZWU9XSldmcUJhcy9rLzF3QWpDZU1DMFRs +dm5maWRcbmxIMHh5ZjE3YVU2UWRqL0hua3ZZQXlMMTEvdVZvT2c2S1VwQ2VW +ZU1JNC9yZjBFZHI4Z1hyNEhMb2loT1xuWkNkdkc5ZjMxUEF3ZUpjUTJ2b3hr +eEJPMFhFdVErVnd6UVgzb2lneVNNUUJEck1QaGc5UTY5eXp6R3hJXG5teHJ6 +YWY2bmdzSk9aS1FnVlllamtpVmdCUnBuUm16T1VOb0U0cXU4Njh4cm9xOUZp +RkorK0JUMnUxc01cbllvOFhNSC9ndGltSTF6YUltdVF4TTk1bE9IcTNLM0hr +UFhoY3ZxT1ZwcW9sUnQ1RE1KelEvOTBMNkFraFxuK3ZXU1hOMThSZXhHUklR +RDBRVUcreWVnSUtxTFY0VHljMEdzNjhYYldoWXQxdytTTlpIank1b1F4MGZq +XG5oU3ZuVzZUdzd4YmtKMWJoUmk0dHpxM1VmNlovUEVDeXZ0ZllOcHFiUDZ3 +N1JQMjBFY3ZTTVR6ZU0yQUNcbjMvcEw5YzNobVVPVzM4dEc3cnJ6cnpLc0ww +M25FZ2NuaVhHZ0J5YTVPdVBsS1JQcG4zdUs1NFVUbmRQaVxuWEhRWjd1WHFC +ekNlNzRDRzJjaHQ3RGxRVDQwdlczMkp0K3RCRDBLakZveWtjajV5dWw5Q1hx +K2VDN0Y5XG5VSXNKR3JRRDFYMGxmdjJ0THdxOTJ4RnhOejdwLzhoUVJ0OG5C +OTNpYWdnUTI4ekdQRTVwbW9WTnEzaW1cbmVlNFQxbktxTFNyRTRKUmRZdTZN +OHBhdk5ZRmFOdGFtbEcxRStvT0tUNG1kMGlNQ0Y0dnlRVzJQc0FuOVxuQVNq +M0lzSTlac0Vra1Q0dTc2cG9RSmliampJKzUxUWFoNnpUSHZLYlVnQ1FtUDla +SEZzajhDdmRabGU2XG44dkUvZmlRd3Z2R2dXaEpFaG96WWEvcW5yQmFWbmd5 +Y1ZqSUFLTm9UbGFyWEYyRDliSjlQUXJJYzdGb0JcbnNBRTY1MDBhM3ZqNzA1 +c2FXcm1kVGRjdXdRU1M3QzY4TDJ4TnFaUlh6T2tRVlR0aWEwUnd0R2xKZm1R +RlxuQ3l3c0tVUHRnbXA4KzNlUUtRWEE4ZTMvdWZOTXhvSENUQ3p6M3ZJaUtY +Y1pVWFRPcEJCVisybXJxOTArXG53eTZ1Q0Y2TkRKVCtkQUU3aE1hMzhSTzhq +cXFVeGR6bktCemJFUDdwNHdWK1Yvb053ajV3SWdhZzBXYm5cblcwUkhpcURx +eVJMNzVNa29JT0g2dndlOW1VbGJ5Q3Fwa1FITDd0Wk45bWZJcXcxWUs5NzY4 +a1czcE1BQVxuMElESDJBRHVTS09NNkpZcjRrYURVNHVNT0E2a1haOFphMmdS +ZUZPYTBxblI5V2E2WFVPeE4vRS9CNVNoXG5qa2RkNWdweGp3MlExa2I5RFhL +TmhJNStjcTF4ekZrMTRReFJjSUNlaTJtWUVtR0w4Rml0SlUxbFg4a2tcbnNO +d2dUbUdja2JaMDluT2FDdjFucUlCcGs5dW4zQnUwMGdiY3hGVGt2VldtTDdw +MVpvUVJJVGQva1FESlxucjBKc3Qvdnk2S1Bkek9xR0Z0M212enEvOUlJL3Br +T2ZuaHJwTWpZY1JkRXB0WVROUWoxVDNBTm82ekFmXG52MmhGM05EQnhMVXdh +bU9kMkljWStaWERVamc5ZGFpUEVLQ1FkMmtGNDJDWXdCKzNQcGw0azVmSk5h +WXFcbnd0SmxNTVkxODFka3Vzbm1qTDVmcmpoMmpnWkZidz09XG4iLCJpdiI6 +IjNiM29OTVJCSHFiRGF3bGdtTEN5RVE9PVxuIn0= +-----END OPENPROJECT-EE TOKEN----- \ No newline at end of file diff --git a/spec/views/admin/enterprises/_current.html.erb_spec.rb b/spec/views/admin/enterprises/_current.html.erb_spec.rb new file mode 100644 index 0000000000..56c087293e --- /dev/null +++ b/spec/views/admin/enterprises/_current.html.erb_spec.rb @@ -0,0 +1,86 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +describe 'admin/enterprises/_current', type: :view do + let(:current_user) { FactoryBot.create :admin } + let(:ee_token) { "v1_expired_with_7_days_reprieve_at_2021_09_01.token" } + let(:current_time) { DateTime.now } + + before do + allow(User).to receive(:current).and_return current_user + + encoded = File.read Rails.root.join("spec/fixtures/ee_tokens/#{ee_token}") + token = EnterpriseToken.new(encoded_token: encoded) + + assign :current_token, token + + Timecop.travel(current_date) do + render :partial => "enterprises/current" + end + end + + context "with token still valid" do + let(:current_date) { "2021-08-28".to_datetime } + + it "renders the token as not expired and with no reprieve days" do + expect(rendered.to_s).to include 'data-is-expired="false"' + expect(rendered.to_s).to include 'data-reprieve-days-left=""' + end + end + + context "with token just expired (within grace period)" do + let(:current_date) { "2021-09-02".to_datetime } + + it "renders the token as expired and with 6 reprieve days" do + expect(rendered.to_s).to include 'data-is-expired="true"' + expect(rendered.to_s).to include 'data-reprieve-days-left="6"' + end + end + + context "with token expired past reprieve" do + let(:current_date) { "2021-09-08".to_datetime } + + it "renders the token as expired and with 0 reprieve days" do + expect(rendered.to_s).to include 'data-is-expired="true"' + expect(rendered.to_s).to include 'data-reprieve-days-left="0"' + end + end + + context "with token expired and no reprieve" do + let(:ee_token) { "v1_expired_without_reprieve_at_2021_09_01.token" } + + let(:current_date) { "2021-09-08".to_datetime } + + it "renders the token as expired and with no reprieve days" do + expect(rendered.to_s).to include 'data-is-expired="true"' + expect(rendered.to_s).to include 'data-reprieve-days-left=""' + end + end +end From 2ab227091e661369b5fd091ed0c55605b80129dd Mon Sep 17 00:00:00 2001 From: Markus Kahl Date: Mon, 20 Sep 2021 14:39:45 +0100 Subject: [PATCH 59/68] bumped openproject-token to 2.2 (for reprieve support) --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 8811e8c279..9588846f66 100644 --- a/Gemfile +++ b/Gemfile @@ -180,7 +180,7 @@ gem 'aws-sdk-core', '~> 3.107' # File upload via fog + screenshots on travis gem 'aws-sdk-s3', '~> 1.91' -gem 'openproject-token', '~> 2.2.0' +gem 'openproject-token', '~> 2.2' gem 'plaintext', '~> 0.3.2' diff --git a/Gemfile.lock b/Gemfile.lock index 7a78061557..4b026c9361 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -628,7 +628,7 @@ GEM validate_email validate_url webfinger (>= 1.0.1) - openproject-token (2.1.3) + openproject-token (2.2.0) activemodel parallel (1.20.1) parallel_tests (3.7.0) @@ -1042,7 +1042,7 @@ DEPENDENCIES openproject-pdf_export! openproject-recaptcha! openproject-reporting! - openproject-token (~> 2.1.1) + openproject-token (~> 2.2) openproject-translations! openproject-two_factor_authentication! openproject-webhooks! From 449cc7ba193483aee965dd51bd9d6b7d6258639e Mon Sep 17 00:00:00 2001 From: OpenProject Actions CI Date: Tue, 21 Sep 2021 03:13:00 +0000 Subject: [PATCH 60/68] update locales from crowdin [ci skip] --- config/locales/crowdin/ar.yml | 14 ++++++++++++ config/locales/crowdin/bg.yml | 14 ++++++++++++ config/locales/crowdin/ca.yml | 14 ++++++++++++ config/locales/crowdin/cs.yml | 14 ++++++++++++ config/locales/crowdin/da.yml | 14 ++++++++++++ config/locales/crowdin/de.yml | 14 ++++++++++++ config/locales/crowdin/el.yml | 14 ++++++++++++ config/locales/crowdin/es.yml | 14 ++++++++++++ config/locales/crowdin/fi.yml | 14 ++++++++++++ config/locales/crowdin/fil.yml | 14 ++++++++++++ config/locales/crowdin/fr.yml | 14 ++++++++++++ config/locales/crowdin/hr.yml | 14 ++++++++++++ config/locales/crowdin/hu.yml | 16 ++++++++++++++ config/locales/crowdin/id.yml | 14 ++++++++++++ config/locales/crowdin/it.yml | 14 ++++++++++++ config/locales/crowdin/ja.yml | 14 ++++++++++++ config/locales/crowdin/js-ar.yml | 2 ++ config/locales/crowdin/js-bg.yml | 2 ++ config/locales/crowdin/js-ca.yml | 2 ++ config/locales/crowdin/js-cs.yml | 2 ++ config/locales/crowdin/js-da.yml | 2 ++ config/locales/crowdin/js-de.yml | 2 ++ config/locales/crowdin/js-el.yml | 2 ++ config/locales/crowdin/js-es.yml | 2 ++ config/locales/crowdin/js-fi.yml | 2 ++ config/locales/crowdin/js-fil.yml | 2 ++ config/locales/crowdin/js-fr.yml | 2 ++ config/locales/crowdin/js-hr.yml | 2 ++ config/locales/crowdin/js-hu.yml | 2 ++ config/locales/crowdin/js-id.yml | 2 ++ config/locales/crowdin/js-it.yml | 2 ++ config/locales/crowdin/js-ja.yml | 2 ++ config/locales/crowdin/js-ko.yml | 2 ++ config/locales/crowdin/js-lt.yml | 34 +++++++++++++++-------------- config/locales/crowdin/js-nl.yml | 2 ++ config/locales/crowdin/js-no.yml | 2 ++ config/locales/crowdin/js-pl.yml | 2 ++ config/locales/crowdin/js-pt.yml | 2 ++ config/locales/crowdin/js-ro.yml | 2 ++ config/locales/crowdin/js-ru.yml | 2 ++ config/locales/crowdin/js-sk.yml | 2 ++ config/locales/crowdin/js-sl.yml | 2 ++ config/locales/crowdin/js-sv.yml | 2 ++ config/locales/crowdin/js-tr.yml | 2 ++ config/locales/crowdin/js-uk.yml | 2 ++ config/locales/crowdin/js-vi.yml | 2 ++ config/locales/crowdin/js-zh-CN.yml | 2 ++ config/locales/crowdin/js-zh-TW.yml | 2 ++ config/locales/crowdin/ko.yml | 14 ++++++++++++ config/locales/crowdin/lt.yml | 16 +++++++++++++- config/locales/crowdin/nl.yml | 14 ++++++++++++ config/locales/crowdin/no.yml | 14 ++++++++++++ config/locales/crowdin/pl.yml | 14 ++++++++++++ config/locales/crowdin/pt.yml | 14 ++++++++++++ config/locales/crowdin/ro.yml | 14 ++++++++++++ config/locales/crowdin/ru.yml | 14 ++++++++++++ config/locales/crowdin/sk.yml | 14 ++++++++++++ config/locales/crowdin/sl.yml | 14 ++++++++++++ config/locales/crowdin/sv.yml | 14 ++++++++++++ config/locales/crowdin/tr.yml | 14 ++++++++++++ config/locales/crowdin/uk.yml | 14 ++++++++++++ config/locales/crowdin/vi.yml | 14 ++++++++++++ config/locales/crowdin/zh-CN.yml | 14 ++++++++++++ config/locales/crowdin/zh-TW.yml | 14 ++++++++++++ 64 files changed, 531 insertions(+), 17 deletions(-) diff --git a/config/locales/crowdin/ar.yml b/config/locales/crowdin/ar.yml index ba31d28595..3a262f8746 100644 --- a/config/locales/crowdin/ar.yml +++ b/config/locales/crowdin/ar.yml @@ -562,6 +562,7 @@ ar: invalid_url: 'is not a valid URL.' invalid_url_scheme: 'is not a supported protocol (allowed: %{allowed_schemes}).' less_than_or_equal_to: "يجب أن يكون أقل من أو يساوي %{count}." + not_available: "is not available due to a system configuration." not_deletable: "cannot be deleted." not_current_user: "is not the current user." not_a_date: "is not a valid date." @@ -581,6 +582,10 @@ ar: unremovable: "cannot be removed." wrong_length: "هو طول خاطئ (يجب أن تكون الأحرف %{count})." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'عالمي' @@ -1278,6 +1283,7 @@ ar: export: your_work_packages_export: "Your work packages export" succeeded: "The export has completed successfully." + failed: "The export has failed: %{message}" format: atom: "ذرّة" csv: "CSV" @@ -2301,6 +2307,7 @@ ar: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "طريقة تسليم البريد الإلكتروني" setting_sendmail_location: "موقع إرسال بريد إلكتروني قابل للتنفيذ" setting_smtp_enable_starttls_auto: "استخدم STARTTLS تلقائيًّا إذا كان متوفرًا" @@ -2415,6 +2422,13 @@ ar: passwords: "كلمات المرور" session: "الجلسة" brute_force_prevention: "حجب المستخدم الآلي" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/bg.yml b/config/locales/crowdin/bg.yml index ec5c8641ca..729ff43e75 100644 --- a/config/locales/crowdin/bg.yml +++ b/config/locales/crowdin/bg.yml @@ -558,6 +558,7 @@ bg: invalid_url: 'невалиден URL адрес.' invalid_url_scheme: 'is not a supported protocol (allowed: %{allowed_schemes}).' less_than_or_equal_to: "трябва да бъде по-малка или равна на %{count}." + not_available: "is not available due to a system configuration." not_deletable: "не може да бъде изтрито." not_current_user: "не е текущият потребител." not_a_date: "is not a valid date." @@ -577,6 +578,10 @@ bg: unremovable: "cannot be removed." wrong_length: "е грешна дължина (трябва да бъде %{count} знаци)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Global' @@ -1214,6 +1219,7 @@ bg: export: your_work_packages_export: "Експорт на вашите работни пакети" succeeded: "Експортирането завърши успешно" + failed: "The export has failed: %{message}" format: atom: "Атом" csv: "CSV" @@ -2235,6 +2241,7 @@ bg: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) позволява разрешения" setting_apiv3_cors_origins_text_html: > Ако CORS е активиран, това са източниците, на които е разрешен достъп до API на OpenProject.
Моля, проверете Документацията в заглавието Origin за това как да посочите очакваните стойности. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Метод за доставка на имейл" setting_sendmail_location: "Местоположение на sendmail програма" setting_smtp_enable_starttls_auto: "Автоматично използвай STARTTLS ако е налично" @@ -2349,6 +2356,13 @@ bg: passwords: "Пароли" session: "Сесия" brute_force_prevention: "Automated user blocking" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/ca.yml b/config/locales/crowdin/ca.yml index 2cb10be872..cec312e08b 100644 --- a/config/locales/crowdin/ca.yml +++ b/config/locales/crowdin/ca.yml @@ -558,6 +558,7 @@ ca: invalid_url: 'is not a valid URL.' invalid_url_scheme: 'is not a supported protocol (allowed: %{allowed_schemes}).' less_than_or_equal_to: "ha de ser menor o igual a %{count}." + not_available: "is not available due to a system configuration." not_deletable: "cannot be deleted." not_current_user: "is not the current user." not_a_date: "is not a valid date." @@ -577,6 +578,10 @@ ca: unremovable: "cannot be removed." wrong_length: "la longitud és incorrecta (haurien de ser %{count} caràcters)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Global' @@ -1214,6 +1219,7 @@ ca: export: your_work_packages_export: "Your work packages export" succeeded: "The export has completed successfully." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2233,6 +2239,7 @@ ca: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Mètode de lliurament del correu electrònic" setting_sendmail_location: "Localització de l'executable de sendmail" setting_smtp_enable_starttls_auto: "Utilitza STARTTLS automàticament si està disponible" @@ -2347,6 +2354,13 @@ ca: passwords: "Contrasenyes" session: "Sessió" brute_force_prevention: "Bloqueig automàtic d'usuari" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/cs.yml b/config/locales/crowdin/cs.yml index 90647f9fdc..29466cc819 100644 --- a/config/locales/crowdin/cs.yml +++ b/config/locales/crowdin/cs.yml @@ -560,6 +560,7 @@ cs: invalid_url: 'není platná adresa URL.' invalid_url_scheme: 'není podporovaný protokol (povoleny: %{allowed_schemes}).' less_than_or_equal_to: "musí být menší než nebo rovno %{count}." + not_available: "is not available due to a system configuration." not_deletable: "cannot be deleted." not_current_user: "není aktuální uživatel." not_a_date: "není platné datum." @@ -579,6 +580,10 @@ cs: unremovable: "nelze odstranit." wrong_length: "má chybnou délku (měla by být %{count} znaků)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Globální' @@ -1246,6 +1251,7 @@ cs: export: your_work_packages_export: "Your work packages export" succeeded: "The export has completed successfully." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2268,6 +2274,7 @@ cs: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Způsob zasílání emailu" setting_sendmail_location: "Umístění spustitelného souboru sendmail" setting_smtp_enable_starttls_auto: "Automaticky použít STARTTLS, pokud je dostupné" @@ -2382,6 +2389,13 @@ cs: passwords: "Hesla" session: "Relace" brute_force_prevention: "Automatizované blokování uživatelů" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/da.yml b/config/locales/crowdin/da.yml index 6147735534..1e9d27570f 100644 --- a/config/locales/crowdin/da.yml +++ b/config/locales/crowdin/da.yml @@ -556,6 +556,7 @@ da: invalid_url: 'is not a valid URL.' invalid_url_scheme: 'is not a supported protocol (allowed: %{allowed_schemes}).' less_than_or_equal_to: "skal være mindre end eller lig med %{count}." + not_available: "is not available due to a system configuration." not_deletable: "Kan ikke slettes" not_current_user: "er ikke den aktuelle bruger." not_a_date: "is not a valid date." @@ -575,6 +576,10 @@ da: unremovable: "cannot be removed." wrong_length: "har forkert længde (burde være %{count} tegn)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Global' @@ -1212,6 +1217,7 @@ da: export: your_work_packages_export: "Your work packages export" succeeded: "The export has completed successfully." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2231,6 +2237,7 @@ da: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Email delivery method" setting_sendmail_location: "Location of the sendmail executable" setting_smtp_enable_starttls_auto: "Automatically use STARTTLS if available" @@ -2345,6 +2352,13 @@ da: passwords: "Adgangskoder" session: "Session" brute_force_prevention: "Automatiseret brugerblokering" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/de.yml b/config/locales/crowdin/de.yml index 0bfa63ca93..3d80ca94ce 100644 --- a/config/locales/crowdin/de.yml +++ b/config/locales/crowdin/de.yml @@ -553,6 +553,7 @@ de: invalid_url: 'ist keine gültige URL.' invalid_url_scheme: 'ist kein unterstütztes Protokoll (erlaubt: %{allowed_schemes}).' less_than_or_equal_to: "muss kleiner oder gleich %{count} sein." + not_available: "ist aufgrund einer Systemkonfiguration nicht verfügbar." not_deletable: "kann nicht entfernt werden." not_current_user: "ist nicht der aktuelle Benutzer." not_a_date: "ist kein gültiges Datum." @@ -572,6 +573,10 @@ de: unremovable: "kann nicht entfernt werden." wrong_length: "hat die falsche Länge (muss genau %{count} Zeichen haben)." models: + attachment: + attributes: + content_type: + not_whitelisted: "ist auf '%{value}' gesetzt, was nicht Teil der Positivliste für Dateiuploads ist." capability: context: global: 'Global' @@ -1209,6 +1214,7 @@ de: export: your_work_packages_export: "Export der Arbeitspakete" succeeded: "Der Export wurde erfolgreich abgeschlossen." + failed: "Export fehlgeschlagen: %{message}" format: atom: "Atom" csv: "CSV" @@ -2230,6 +2236,7 @@ de: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) erlaubte Origins" setting_apiv3_cors_origins_text_html: > Wenn CORS aktiviert ist, sind dies die Origins, die auf die OpenProject API zugreifen dürfen.
Bitte überprüfen Sie die Dokumentation über den Origin Header, wie die Werte anzugeben sind. + setting_attachment_whitelist: "Positivliste für Dateiuploads" setting_email_delivery_method: "E-Mail Zustellungsoption" setting_sendmail_location: "Systempfad zu sendmail" setting_smtp_enable_starttls_auto: "Automatisch STARTTLS verwenden, falls vorhanden" @@ -2344,6 +2351,13 @@ de: passwords: "Passwörter" session: "Sitzung" brute_force_prevention: "Automatisches Sperren von Benutzern" + attachments: + whitelist_text_html: > + Legen Sie eine Liste gültiger Dateierweiterungen und/oder Mime-Typen für hochgeladene Dateien fest.
Dateierweiterungen (z. B. %{ext_example}) oder Mime-Typen (z. ., %{mime_example}).
Lassen Sie diese Liste leer, um das Hochladen beliebiger Dateitypen zu erlauben. Mehrere Werte erlaubt (eine Zeile pro Wert). + notifications: + retention_text: > + Legen Sie die Anzahl der Tage fest, in denen Benachrichtigungen für Benutzer im System gehalten werden. Alle Ereignisse, die älter als diese Zeit sind, werden gelöscht und stehen nicht mehr als In-App-Benachrichtigungen zur Verfügung. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > Wenn beide Optionen "%{day_of_week_setting_name}" oder "%{first_week_setting_name}" gesetzt sind der andere muss ebenfalls gesetzt werden, um Ungereimtheiten im Frontend zu vermeiden. diff --git a/config/locales/crowdin/el.yml b/config/locales/crowdin/el.yml index d2ce363894..cf5b2e69ba 100644 --- a/config/locales/crowdin/el.yml +++ b/config/locales/crowdin/el.yml @@ -553,6 +553,7 @@ el: invalid_url: 'δεν είναι μια έγκυρη διεύθυνση URL.' invalid_url_scheme: 'δεν είναι υποστηριζόμενο πρωτόκολλο (επιτρεπόμενα: %{allowed_schemes}).' less_than_or_equal_to: "πρέπει να είναι μικρότερο ή ίσο με %{count}." + not_available: "is not available due to a system configuration." not_deletable: "δεν μπορεί να διαγραφεί." not_current_user: "δεν είναι ο τρέχων χρήστης." not_a_date: "δεν είναι έγκυρη ημερομηνία." @@ -572,6 +573,10 @@ el: unremovable: "δεν μπορεί να αφαιρεθεί." wrong_length: "έχει λάθος μέγεθος (πρέπει να είναι %{count} χαρακτήρες)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Καθολικό' @@ -1209,6 +1214,7 @@ el: export: your_work_packages_export: "Η εξαγωγή των πακέτων εργασίας σας" succeeded: "Η εξαγωγή ολοκληρώθηκε επιτυχώς." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2229,6 +2235,7 @@ el: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) επιτρεπόμενες προελεύσεις" setting_apiv3_cors_origins_text_html: > Εάν είναι ενεργοποιημένο το CORS, αυτές είναι οι πηγές που επιτρέπεται να έχουν πρόσβαση στο OpenProject API.
Παρακαλώ ελέγξτε το Τεκμηρίωση στην κεφαλίδα προέλευσης για τον τρόπο καθορισμού των αναμενόμενων τιμών. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Μέθοδος αποστολής email" setting_sendmail_location: "Τοποθεσία του εκτελέσιμου sendmail" setting_smtp_enable_starttls_auto: "Αυτόματη χρήση STARTTLS αν είναι διαθέσιμο" @@ -2343,6 +2350,13 @@ el: passwords: "Κωδικοί πρόσβασης" session: "Συνεδρία" brute_force_prevention: "Αυτόματο μπλοκάρισμα χρήστη" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/es.yml b/config/locales/crowdin/es.yml index 80924cf96f..58070d098f 100644 --- a/config/locales/crowdin/es.yml +++ b/config/locales/crowdin/es.yml @@ -555,6 +555,7 @@ es: invalid_url: 'no es una URL válida.' invalid_url_scheme: 'no es un protocolo admitido (permitidos: %{allowed_schemes}).' less_than_or_equal_to: "debe ser menor o igual a %{count}." + not_available: "is not available due to a system configuration." not_deletable: "no se puede eliminar." not_current_user: "no es el usuario actual." not_a_date: "no es una fecha válida." @@ -574,6 +575,10 @@ es: unremovable: "no puede ser eliminado." wrong_length: "la longitud es incorrecta (debe ser %{count} caracteres)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Global' @@ -1211,6 +1216,7 @@ es: export: your_work_packages_export: "Exportar paquetes de trabajo" succeeded: "La exportación se ha completado correctamente." + failed: "The export has failed: %{message}" format: atom: "Atomo" csv: "CSV" @@ -2232,6 +2238,7 @@ es: setting_apiv3_cors_origins: "Orígenes permitidos de la API de CORS (uso compartido de recursos entre orígenes) V3" setting_apiv3_cors_origins_text_html: > Si CORS está habilitado, estos son los orígenes que tienen permiso para acceder a la API de OpenProject.
Consulte la documentación sobre el encabezado Origin para obtener información sobre cómo especificar los valores esperados. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Método de envío de correo electrónico" setting_sendmail_location: "Ubicación del ejecutable de sendmail" setting_smtp_enable_starttls_auto: "Usar STARTTLS automáticamente si está disponible" @@ -2346,6 +2353,13 @@ es: passwords: "Contraseñas" session: "Sesión" brute_force_prevention: "Bloqueo automático de usuarios" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Establecer el número de días que los eventos de notificación para los usuarios (la fuente de las notificaciones dentro de la aplicación) se mantendrá en el sistema. Cualquier evento anterior a este tiempo será eliminado. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > Si se establece una de las opciones «%{day_of_week_setting_name}» o «%{first_week_setting_name}», también debe establecerse la otra para evitar incoherencias en el front-end. diff --git a/config/locales/crowdin/fi.yml b/config/locales/crowdin/fi.yml index 0e0daae5aa..faeb050c4e 100644 --- a/config/locales/crowdin/fi.yml +++ b/config/locales/crowdin/fi.yml @@ -558,6 +558,7 @@ fi: invalid_url: 'ei ole kelvollinen URL.' invalid_url_scheme: 'is not a supported protocol (allowed: %{allowed_schemes}).' less_than_or_equal_to: "täytyy olla pienempi tai yhtä suuri kuin %{count}." + not_available: "is not available due to a system configuration." not_deletable: "cannot be deleted." not_current_user: "is not the current user." not_a_date: "ei ole kelvollinen päivämäärä." @@ -577,6 +578,10 @@ fi: unremovable: "ei voida poistaa." wrong_length: "on väärän pituinen (täytyy olla täsmälleen %{count} merkkiä)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Yleinen' @@ -1214,6 +1219,7 @@ fi: export: your_work_packages_export: "Your work packages export" succeeded: "The export has completed successfully." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2235,6 +2241,7 @@ fi: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Sähköpostiviestien toimitustapa" setting_sendmail_location: "Location of the sendmail executable" setting_smtp_enable_starttls_auto: "Automatically use STARTTLS if available" @@ -2349,6 +2356,13 @@ fi: passwords: "Salasana" session: "Istunto" brute_force_prevention: "Automaattinen käyttäjän estäminen" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/fil.yml b/config/locales/crowdin/fil.yml index 1c1949b15f..f7e7c1a933 100644 --- a/config/locales/crowdin/fil.yml +++ b/config/locales/crowdin/fil.yml @@ -558,6 +558,7 @@ fil: invalid_url: 'ay hindi balidong URL.' invalid_url_scheme: 'ay hindi suportado ng protocol (pinahintulutan: %{allowed_schemes}).' less_than_or_equal_to: "dapat ay hindi baba o katumbas sa %{count}." + not_available: "is not available due to a system configuration." not_deletable: "cannot be deleted." not_current_user: "is not the current user." not_a_date: "ay hindi balido ang petsa." @@ -577,6 +578,10 @@ fil: unremovable: "cannot be removed." wrong_length: "ay ang maling haba (dapat ay %{count} ang mga karakter)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Global' @@ -1214,6 +1219,7 @@ fil: export: your_work_packages_export: "Your work packages export" succeeded: "The export has completed successfully." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2233,6 +2239,7 @@ fil: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Pamaraan ng paghahatid ng email" setting_sendmail_location: "Lokasyon ng sendmail na pagpapatupad" setting_smtp_enable_starttls_auto: "Automatikong gagamitin ang STARTTLS kung magagamit" @@ -2347,6 +2354,13 @@ fil: passwords: "Ang mga password" session: "Sesyon" brute_force_prevention: "Naka-automate na user block" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/fr.yml b/config/locales/crowdin/fr.yml index 4b0a990302..ed244e3bc9 100644 --- a/config/locales/crowdin/fr.yml +++ b/config/locales/crowdin/fr.yml @@ -557,6 +557,7 @@ fr: invalid_url: 'n’est pas une URL valide.' invalid_url_scheme: 'n’est pas un protocole pris en charge (autorisés : %{allowed_schemes}).' less_than_or_equal_to: "doit être inférieur ou égal à %{count}." + not_available: "is not available due to a system configuration." not_deletable: "ne peut pas être supprimé" not_current_user: "n'est pas l'utilisateur actuel." not_a_date: "n'est pas une date valide." @@ -576,6 +577,10 @@ fr: unremovable: "ne peut pas être supprimé." wrong_length: "est de mauvaise longueur (devrait être %{count} caractères)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Global' @@ -1213,6 +1218,7 @@ fr: export: your_work_packages_export: "Exportation de vos lots de travaux" succeeded: "Export terminé avec succès" + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2234,6 +2240,7 @@ fr: setting_apiv3_cors_origins: "Origines autorisées de l'API V3 Cross-Origin Resource Sharing (CORS)" setting_apiv3_cors_origins_text_html: > Si CORS est activé, ce sont les origines qui sont autorisées à accéder à l'API OpenProject.
Veuillez vérifier la documentation sur l'en-tête Origin sur la façon de spécifier les valeurs attendues. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Méthode de livraison d'e-mail" setting_sendmail_location: "Emplacement de l’exécutable sendmail" setting_smtp_enable_starttls_auto: "Utiliser automatiquement STARTTLS si disponible" @@ -2348,6 +2355,13 @@ fr: passwords: "Mots de passe" session: "Session" brute_force_prevention: "Blocage d'utilisateur automatique (pour éviter les pirates)" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > Si une des options « %{day_of_week_setting_name} » ou « %{first_week_setting_name} » sont définies, l'autre doit être réglé aussi bien pour éviter les incohérences dans le frontend. diff --git a/config/locales/crowdin/hr.yml b/config/locales/crowdin/hr.yml index 7763be9a85..8aa5b3614f 100644 --- a/config/locales/crowdin/hr.yml +++ b/config/locales/crowdin/hr.yml @@ -559,6 +559,7 @@ hr: invalid_url: 'nije valjani URL.' invalid_url_scheme: 'nije podržani protokol (dozvoljeno: %{allowed_schemes}).' less_than_or_equal_to: "mora biti manji od ili jednak %{count}." + not_available: "is not available due to a system configuration." not_deletable: "cannot be deleted." not_current_user: "is not the current user." not_a_date: "is not a valid date." @@ -578,6 +579,10 @@ hr: unremovable: "cannot be removed." wrong_length: "je krive duljine (mora biti %{count} znakova)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Globalne' @@ -1230,6 +1235,7 @@ hr: export: your_work_packages_export: "Your work packages export" succeeded: "The export has completed successfully." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2252,6 +2258,7 @@ hr: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Email delivery method" setting_sendmail_location: "Location of the sendmail executable" setting_smtp_enable_starttls_auto: "Automatically use STARTTLS if available" @@ -2366,6 +2373,13 @@ hr: passwords: "Lozinke" session: "Sesija" brute_force_prevention: "Automatizirano blokiranje korisnika" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/hu.yml b/config/locales/crowdin/hu.yml index 48ef4bc801..bbb77964ca 100644 --- a/config/locales/crowdin/hu.yml +++ b/config/locales/crowdin/hu.yml @@ -558,6 +558,7 @@ hu: invalid_url: 'nem érvényes URL.' invalid_url_scheme: 'nem támogatott protokoll (támogatott: %{allowed_schemes}).' less_than_or_equal_to: "kisebbnek vagy egyenlőnek kell lennie mint, %{count}." + not_available: "nem érhető el a rendszer konfigurációja miatt.\n" not_deletable: "nem törölhető" not_current_user: "nem az aktuális felhasználó" not_a_date: "nem érvényes dátum." @@ -577,6 +578,10 @@ hu: unremovable: "nem törölhető." wrong_length: "nem megfelelő hosszúságú (%{count} karakter szükséges)." models: + attachment: + attributes: + content_type: + not_whitelisted: "'%{value}'-re van állítva, ami nem szerepel a feltöltés fehér listáján.\n" capability: context: global: 'Globális' @@ -1218,6 +1223,7 @@ hu: export: your_work_packages_export: "Munkacsomagok exportálása" succeeded: "Az exportálás sikeresen végetért." + failed: "Az exportálás meghiúsult %{message}" format: atom: "Atom" csv: "CSV" @@ -2239,6 +2245,7 @@ hu: setting_apiv3_cors_origins_text_html: > Ha a CORS engedélyezve van, ezek azok az eredetiek, amelyek hozzáférhetnek az OpenProject API-hoz.
Kérjük, olvassa el a Dokumentáció az Origin fejlécről című dokumentumot az elvárt értékek megadásáról. + setting_attachment_whitelist: "Mellékletfeltöltési fehérlista" setting_email_delivery_method: "E-mail küldés módja" setting_sendmail_location: "A sendmail program elérési útja" setting_smtp_enable_starttls_auto: "Automatikus STARTTLS, ha rendelkezésre áll" @@ -2353,6 +2360,15 @@ hu: passwords: "Jelszavak" session: "Munkamenet" brute_force_prevention: "Felhasználó automatikus blokkolása" + attachments: + whitelist_text_html: > + Határozza meg a feltöltött fájlok érvényes fájlkiterjesztéseinek és/vagy mime típusainak listáját.
Adjon meg fájlkiterjesztéseket (pl. %{ext_example} ) vagy mime típusokat (pl. %{mime_example} ).
Hagyja üresen, ha bármilyen fájltípust szeretne feltölteni. Több érték megengedett (egy sor minden értékhez). + + notifications: + retention_text: > + Állítsa be, hogy a felhasználók hány napig tartsák az értesítési eseményeket (az alkalmazáson belüli értesítések forrása) a rendszerben. Az ennél régebbi események törlődnek. + + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > Ha a "%{day_of_week_setting_name}" vagy a "%{first_week_setting_name}" opciók közül valamelyik be van állítva, akkor a másiknak is be kell lennie, hogy elkerüljük az ellentmondásokat a frontendben. diff --git a/config/locales/crowdin/id.yml b/config/locales/crowdin/id.yml index 02889db584..e73598d927 100644 --- a/config/locales/crowdin/id.yml +++ b/config/locales/crowdin/id.yml @@ -553,6 +553,7 @@ id: invalid_url: 'bukanlah URL yang Valid.' invalid_url_scheme: 'bukanlah sebuah protokol yang didukung (diperbolehkan: %{allowed_schemes}).' less_than_or_equal_to: "harus kurang dari atau sama dengan %{count}." + not_available: "is not available due to a system configuration." not_deletable: "cannot be deleted." not_current_user: "is not the current user." not_a_date: "bukan tanggal yang valid." @@ -572,6 +573,10 @@ id: unremovable: "tidak bisa dihapus." wrong_length: "panjang tidak sesuai (harus %{count} karakter)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Global' @@ -1194,6 +1199,7 @@ id: export: your_work_packages_export: "Your work packages export" succeeded: "The export has completed successfully." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2212,6 +2218,7 @@ id: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Email delivery method" setting_sendmail_location: "Location of the sendmail executable" setting_smtp_enable_starttls_auto: "Automatically use STARTTLS if available" @@ -2326,6 +2333,13 @@ id: passwords: "Password" session: "Sesi" brute_force_prevention: "Pemblokiran otomatis user" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/it.yml b/config/locales/crowdin/it.yml index 173741c45c..4db1cfedbb 100644 --- a/config/locales/crowdin/it.yml +++ b/config/locales/crowdin/it.yml @@ -554,6 +554,7 @@ it: invalid_url: 'non è un URL valido.' invalid_url_scheme: 'non è un protocollo supportato (ammessi: %{allowed_schemes}).' less_than_or_equal_to: "deve essere inferiore o uguale a %{count}." + not_available: "non è disponibile a causa di una configurazione di sistema." not_deletable: "non può essere eliminato." not_current_user: "non è l'utente attuale." not_a_date: "non è una data valida." @@ -573,6 +574,10 @@ it: unremovable: "non può essere rimosso." wrong_length: "è della lunghezza sbagliata (dovrebbe essere %{count} caratteri)." models: + attachment: + attributes: + content_type: + not_whitelisted: "è impostato a '%{value}', che non è autorizzato per il caricamento." capability: context: global: 'Globale' @@ -1210,6 +1215,7 @@ it: export: your_work_packages_export: "Esportazione macro-attività" succeeded: "L’esportazione si è conclusa con successo." + failed: "L'esportazione non è riuscita: %{message}" format: atom: "Atom" csv: "CSV" @@ -2231,6 +2237,7 @@ it: setting_apiv3_cors_origins: "Origini di condivisione CORS (Cross-Origin Resource Sharing) API V3 consentite" setting_apiv3_cors_origins_text_html: > Se CORS è abilitato, queste sono le origini che possono accedere alle API di OpenProject.
Controlla la Documentazione sull'intestazione dell'origine per sapere come specificare i valori previsti. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Metodo di invio e-mail" setting_sendmail_location: "Percorso dell'eseguibile del programma di invio della posta" setting_smtp_enable_starttls_auto: "Usa automaticamente, se disponibile, STARTTLS" @@ -2345,6 +2352,13 @@ it: passwords: "Password" session: "Sessione" brute_force_prevention: "Blocco automatico degli utenti" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Impostare il numero di giorni di notifica per gli utenti (la fonte per le notifiche in-app) sarà mantenuto nel sistema. Eventuali eventi più vecchi verranno eliminati. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > Se le opzioni "%{day_of_week_setting_name}" o "%{first_week_setting_name}" sono impostate, dev'esser impostata anche l'altra per evitare incoerenze in vetrina. diff --git a/config/locales/crowdin/ja.yml b/config/locales/crowdin/ja.yml index a9995d163e..fd65da24a1 100644 --- a/config/locales/crowdin/ja.yml +++ b/config/locales/crowdin/ja.yml @@ -553,6 +553,7 @@ ja: invalid_url: 'は有効な URL ではありません。' invalid_url_scheme: 'はサポートされたプロトコルではありません (可能: %{allowed_schemes})。' less_than_or_equal_to: "は%{count}以下の値にしてください。" + not_available: "is not available due to a system configuration." not_deletable: "削除できません。" not_current_user: "現在のユーザーではありません。" not_a_date: "は有効な日付ではありません。" @@ -572,6 +573,10 @@ ja: unremovable: "削除できません。" wrong_length: "は%{count}文字を入力してください。" models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: '全般' @@ -1194,6 +1199,7 @@ ja: export: your_work_packages_export: "ワークパッケージをエクスポート" succeeded: "エクスポートが正常に完了しました。" + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2214,6 +2220,7 @@ ja: setting_apiv3_cors_origins: "API V3のオリジン間リソース共有 (CORS) が許可されています" setting_apiv3_cors_origins_text_html: > CORS が有効になっている場合、これらは OpenProject API へのアクセスが許可されている起源です。
期待される値を指定する方法については、 Origin ヘッダー のドキュメントを確認してください。 + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "メールの配信方法" setting_sendmail_location: "Sendmail の実行可能ファイルの場所" setting_smtp_enable_starttls_auto: "利用可能な場合、STARTTLS を自動的に使用する" @@ -2328,6 +2335,13 @@ ja: passwords: "パスワード" session: "セッション" brute_force_prevention: "自動的にユーザをロック" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > オプション "%{day_of_week_setting_name}" または "%{first_week_setting_name}" が設定されている場合、フロントエンドでの不整合を避けるために、もう一方も設定する必要があります。 diff --git a/config/locales/crowdin/js-ar.yml b/config/locales/crowdin/js-ar.yml index 4d7082cd65..4b4982b31a 100644 --- a/config/locales/crowdin/js-ar.yml +++ b/config/locales/crowdin/js-ar.yml @@ -185,6 +185,8 @@ ar: new_group: 'مجموعة جديدة' reset_to_defaults: 'Reset to defaults' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation of email address" confirmation_info: > diff --git a/config/locales/crowdin/js-bg.yml b/config/locales/crowdin/js-bg.yml index 520392f562..4f3370ae0a 100644 --- a/config/locales/crowdin/js-bg.yml +++ b/config/locales/crowdin/js-bg.yml @@ -185,6 +185,8 @@ bg: new_group: 'Нова група' reset_to_defaults: 'Reset to defaults' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation of email address" confirmation_info: > diff --git a/config/locales/crowdin/js-ca.yml b/config/locales/crowdin/js-ca.yml index 41adcb7410..10fa1dd758 100644 --- a/config/locales/crowdin/js-ca.yml +++ b/config/locales/crowdin/js-ca.yml @@ -185,6 +185,8 @@ ca: new_group: 'Nou grup' reset_to_defaults: 'Reset to defaults' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation of email address" confirmation_info: > diff --git a/config/locales/crowdin/js-cs.yml b/config/locales/crowdin/js-cs.yml index f0b83796a9..46bf91704a 100644 --- a/config/locales/crowdin/js-cs.yml +++ b/config/locales/crowdin/js-cs.yml @@ -185,6 +185,8 @@ cs: new_group: 'Nová skupina' reset_to_defaults: 'Obnovit výchozí nastavení' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation of email address" confirmation_info: > diff --git a/config/locales/crowdin/js-da.yml b/config/locales/crowdin/js-da.yml index 7280e829a4..046df7bea5 100644 --- a/config/locales/crowdin/js-da.yml +++ b/config/locales/crowdin/js-da.yml @@ -184,6 +184,8 @@ da: new_group: 'Ny gruppe' reset_to_defaults: 'Reset to defaults' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Bekræft mailadresse" confirmation_info: > diff --git a/config/locales/crowdin/js-de.yml b/config/locales/crowdin/js-de.yml index 84b2e60d35..76017a1c85 100644 --- a/config/locales/crowdin/js-de.yml +++ b/config/locales/crowdin/js-de.yml @@ -184,6 +184,8 @@ de: new_group: 'Neue Gruppe' reset_to_defaults: 'Auf Standardwerte zurücksetzen' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "E-Mail-Adresse bestätigen" confirmation_info: > diff --git a/config/locales/crowdin/js-el.yml b/config/locales/crowdin/js-el.yml index 046c6ef7c2..32319c5192 100644 --- a/config/locales/crowdin/js-el.yml +++ b/config/locales/crowdin/js-el.yml @@ -184,6 +184,8 @@ el: new_group: 'Νέα ομάδα' reset_to_defaults: 'Επαναφορά στα προεπιλεγμένα' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Επιβεβαίωση της διεύθυνσης email" confirmation_info: > diff --git a/config/locales/crowdin/js-es.yml b/config/locales/crowdin/js-es.yml index b9e8883228..79529be1c8 100644 --- a/config/locales/crowdin/js-es.yml +++ b/config/locales/crowdin/js-es.yml @@ -185,6 +185,8 @@ es: new_group: 'Nuevo grupo' reset_to_defaults: 'Restablecer valores predeterminados' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmación de dirección de correo electrónico" confirmation_info: > diff --git a/config/locales/crowdin/js-fi.yml b/config/locales/crowdin/js-fi.yml index cc26a6f25b..71a92f6323 100644 --- a/config/locales/crowdin/js-fi.yml +++ b/config/locales/crowdin/js-fi.yml @@ -185,6 +185,8 @@ fi: new_group: 'Uusi ryhmä' reset_to_defaults: 'Reset to defaults' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation of email address" confirmation_info: > diff --git a/config/locales/crowdin/js-fil.yml b/config/locales/crowdin/js-fil.yml index ff3f5bfef7..f5b4968192 100644 --- a/config/locales/crowdin/js-fil.yml +++ b/config/locales/crowdin/js-fil.yml @@ -185,6 +185,8 @@ fil: new_group: 'Bagong grupo' reset_to_defaults: 'I-reset sa mga default' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation of email address" confirmation_info: > diff --git a/config/locales/crowdin/js-fr.yml b/config/locales/crowdin/js-fr.yml index eebb3d6ec2..40a39eed8d 100644 --- a/config/locales/crowdin/js-fr.yml +++ b/config/locales/crowdin/js-fr.yml @@ -185,6 +185,8 @@ fr: new_group: 'Nouveau groupe' reset_to_defaults: 'Rétablir les valeurs par défaut' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation de l'adresse e-mail" confirmation_info: > diff --git a/config/locales/crowdin/js-hr.yml b/config/locales/crowdin/js-hr.yml index 305032ff7e..0d8bb39a19 100644 --- a/config/locales/crowdin/js-hr.yml +++ b/config/locales/crowdin/js-hr.yml @@ -185,6 +185,8 @@ hr: new_group: 'Nova grupa' reset_to_defaults: 'Reset to defaults' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation of email address" confirmation_info: > diff --git a/config/locales/crowdin/js-hu.yml b/config/locales/crowdin/js-hu.yml index 3a8be9556b..e9cd997463 100644 --- a/config/locales/crowdin/js-hu.yml +++ b/config/locales/crowdin/js-hu.yml @@ -186,6 +186,8 @@ hu: new_group: 'Új csoport' reset_to_defaults: 'Alapbeállítások visszaállítása' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "E-mail cím megerősítése" confirmation_info: > diff --git a/config/locales/crowdin/js-id.yml b/config/locales/crowdin/js-id.yml index e0c94af264..82d99b90b6 100644 --- a/config/locales/crowdin/js-id.yml +++ b/config/locales/crowdin/js-id.yml @@ -185,6 +185,8 @@ id: new_group: 'Grup baru' reset_to_defaults: 'Disetel ulang ke kondisi bawaan' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation of email address" confirmation_info: > diff --git a/config/locales/crowdin/js-it.yml b/config/locales/crowdin/js-it.yml index 7af30ddeaa..689a89c123 100644 --- a/config/locales/crowdin/js-it.yml +++ b/config/locales/crowdin/js-it.yml @@ -185,6 +185,8 @@ it: new_group: 'Nuovo gruppo' reset_to_defaults: 'Ripristino predefinite' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Conferma l'indirizzo email" confirmation_info: > diff --git a/config/locales/crowdin/js-ja.yml b/config/locales/crowdin/js-ja.yml index 9164a5b9fe..4d90bb74a7 100644 --- a/config/locales/crowdin/js-ja.yml +++ b/config/locales/crowdin/js-ja.yml @@ -186,6 +186,8 @@ ja: new_group: '新規グループ' reset_to_defaults: 'デフォルトに戻す' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "メールアドレスの確認" confirmation_info: > diff --git a/config/locales/crowdin/js-ko.yml b/config/locales/crowdin/js-ko.yml index 6680807c30..99c667dd07 100644 --- a/config/locales/crowdin/js-ko.yml +++ b/config/locales/crowdin/js-ko.yml @@ -185,6 +185,8 @@ ko: new_group: '새 그룹' reset_to_defaults: '기본값으로 초기화' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "이메일 주소 확인" confirmation_info: > diff --git a/config/locales/crowdin/js-lt.yml b/config/locales/crowdin/js-lt.yml index 15126683ef..82a869c31f 100644 --- a/config/locales/crowdin/js-lt.yml +++ b/config/locales/crowdin/js-lt.yml @@ -71,13 +71,13 @@ lt: button_collapse_all: "Sutraukti visus" button_expand_all: "Išskleisti visus" button_advanced_filter: "Sudėtingesnis filtras" - button_list_view: "Sąrašo rodinys" - button_show_view: "Viso ekrano rodinys" + button_list_view: "Sąrašo vaizdas" + button_show_view: "Viso ekrano vaizdas" button_log_time: "Registruoti laiką" button_more: "Daugiau" button_open_details: "Atidaryti detalesnį vaizdą" button_close_details: "Uždaryti detalų vaizdavimą" - button_open_fullscreen: "Atidaryti viso ekrano rodinį" + button_open_fullscreen: "Atidaryti viso ekrano vaizdą" button_show_cards: "Rodyti kortelių vaizdą" button_show_list: "Rodyti sąrašo vaizdą" button_quote: "Cituoti" @@ -185,6 +185,8 @@ lt: new_group: 'Nauja grupė' reset_to_defaults: 'Atkurti į numatytuosius' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Patvirtinti el. pašto adresą" confirmation_info: > @@ -244,8 +246,8 @@ lt: error: internal: "Įvyko vidinė klaida." cannot_save_changes_with_message: "Nepavyko įrašyti jūsų pakeitimų dėl šios klaidos: %{error}" - query_saving: "Nepavyko įrašyti rodinio." - embedded_table_loading: "Nepavyko įkelti įdėti rodinio: %{message}" + query_saving: "Nepavyko įrašyti vaizdo." + embedded_table_loading: "Nepavyko įkelti įdėto vaizdo: %{message}" enumeration_activities: "Veikla (laiko sekimas)" enumeration_doc_categories: "Dokumentų kategorijos" enumeration_work_package_priorities: "Darbų paketo prioritetai" @@ -273,7 +275,7 @@ lt: hal: error: update_conflict_refresh: "Spauskite čia resurso atnaujinimui iki naujausios versijos." - edit_prohibited: "%{attribute} redagavimas šiam resursui yra blokuojamas. Arba šis atributas atsiradęs iš ryšių (pvz., vaiko ryšio), arba jis nėra galimas konfigūruoti." + edit_prohibited: "%{attribute} redagavimas šiam resursui yra blokuojamas. Arba šis atributas kilęs iš ryšių (pvz., vaiko ryšio), arba jis nėra konfigūruojamas." format: date: "%{attribute} nėra tinkama data - YYYY-MM-DD tikimasi." general: "Įvyko klaida." @@ -281,7 +283,7 @@ lt: blocks: new_features: text_new_features: "Skaitykite apie naujas savybes ir produkto atnaujinimus." - learn_about: "Sužinokite apie naujas galimybes" + learn_about: "Sužinokite daugiau apie naujas galimybes" standard: learn_about_link: https://www.openproject.org/openproject-11-3-release current_new_feature_html: > @@ -405,7 +407,7 @@ lt: label_select_watcher: "Parinkti stebėtoją..." label_selected_filter_list: "Parinkti filtrus" label_show_attributes: "Rodyti visus atributus" - label_show_in_menu: "Rodyti rodinį meniu" + label_show_in_menu: "Rodyti vaizdą meniu" label_sort_by: "Rūšiuoti pagal" label_sorted_by: "surikiuota pagal" label_sort_higher: "Perkelti aukštyn" @@ -468,7 +470,7 @@ lt: label_upload_counter: "%{done} iš %{count} failų sudoroti" label_validation_error: "Darbų paketas negali būti įrašyti dėl šių klaidų:" label_version_plural: "Versijos" - label_view_has_changed: "Šiame rodinyje yra neįrašytų pakeitimų. Spauskite, kad juos įrašytumėte." + label_view_has_changed: "Šiame vaizde yra neįrašytų pakeitimų. Spauskite, kad juos įrašytumėte." help_texts: show_modal: 'Rodyti atributo pagalbos teksto įrašą' onboarding: @@ -571,8 +573,8 @@ lt: duplicated: "Dubliuotas" blocks: "Blokai" blocked: "Blokuojamas" - precedes: "Ankstesnė" - follows: "Seka" + precedes: "Prieš" + follows: "Po" includes: "Turi savyje" partof: "Dalis" requires: "Reikalingas" @@ -820,9 +822,9 @@ lt: confirm_edit_cancel: "Ar tikrai norite atšaukti pavadinimo pakeitimus? Pavadinimas bus atstatytas į prieš tai buvusį tekstą." click_to_edit_query_name: "Paspauskite norėdami keisti vaizdo pavadinimą." rename_query_placeholder: "Vaizdo pavadinimas" - star_text: "Pažymėti šį rodinį kaip mėgstamiausią ir pridėti į įrašytų rodinių dėklą kairėje." + star_text: "Pažymėti šį vaizdą kaip mėgstamiausią ir pridėti į įrašytų vaizdų dėklą kairėje." public_text: > - Publikuoti šį rodinį, leidžiant kitiems naudotojams prieigą prie jūsų rodinio. Naudotojai su teise „Valdyti viešus rodinius“ galės keisti ar išimti viešą užklausą. Tai neįtakoja darbo paketo rezultatų matomumo rodinyje ir priklausomai nuo teisių, naudotojai gali matyti kitokius rezultatus. + Publikuoti šį vaizdą, leidžiant kitiems naudotojams prieigą prie jūsų vaizdo. Naudotojai su teise „Valdyti viešus vaizdus“ galės keisti ar išimti viešą užklausą. Tai neįtakoja darbo paketo rezultatų matomumo vaizde ir priklausomai nuo teisių, naudotojai gali matyti kitokius rezultatus. errors: unretrievable_query: "Nepavyksta gauti vaizdo iš URL" not_found: "Nėra tokio vaizdo" @@ -922,7 +924,7 @@ lt: destroy_time_entry: title: "Patvirtinkite laiko įrašo trynimą." text: "Ar Jūs tikrai norite ištrinti šį laiko įrašą?" - notice_no_results_to_display: "Nėra rezultatų." + notice_no_results_to_display: "Nėra matomų rezultatų." notice_successful_create: "Sėkmingas sukūrimas." notice_successful_delete: "Sėkmingas panaikinimas." notice_successful_update: "Sėkmingai atnaujinta." @@ -950,7 +952,7 @@ lt: messages_on_field: 'Šio laukelio reikšmė netinkama: %{messages}' error_could_not_resolve_version_name: "Nepavyko nustatyti versijos pavadinimo" error_could_not_resolve_user_name: "Nepavyko nustatyti vartotojo vardo" - error_attachment_upload: "Nepavyko įkelti: %{error}" + error_attachment_upload: "Nepavyko įkelti failo: %{error}" error_attachment_upload_permission: "Jūs neturite teisės įkelti failus į šį resursą." units: workPackage: @@ -966,7 +968,7 @@ lt: hour: one: "1 val." other: "%{count} val" - zero: "0 h" + zero: "0 val" zen_mode: button_activate: 'Įjungti Zen režimą' button_deactivate: 'Išjungti Zen režimą' diff --git a/config/locales/crowdin/js-nl.yml b/config/locales/crowdin/js-nl.yml index 0bb7c086b0..6f80acd0fd 100644 --- a/config/locales/crowdin/js-nl.yml +++ b/config/locales/crowdin/js-nl.yml @@ -185,6 +185,8 @@ nl: new_group: 'Nieuwe groep' reset_to_defaults: 'Standaardinstellingen herstellen' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation of email address" confirmation_info: > diff --git a/config/locales/crowdin/js-no.yml b/config/locales/crowdin/js-no.yml index 6003f94f24..55cd9de65f 100644 --- a/config/locales/crowdin/js-no.yml +++ b/config/locales/crowdin/js-no.yml @@ -185,6 +185,8 @@ new_group: 'Ny gruppe' reset_to_defaults: 'Reset to defaults' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation of email address" confirmation_info: > diff --git a/config/locales/crowdin/js-pl.yml b/config/locales/crowdin/js-pl.yml index 04f1571da6..57c9bddf67 100644 --- a/config/locales/crowdin/js-pl.yml +++ b/config/locales/crowdin/js-pl.yml @@ -185,6 +185,8 @@ pl: new_group: 'Nowa Grupa' reset_to_defaults: 'Resetuj do ustawień domyślnych' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Potwierdzenie adresu e-mail" confirmation_info: > diff --git a/config/locales/crowdin/js-pt.yml b/config/locales/crowdin/js-pt.yml index fd893cd6bd..8bf233147c 100644 --- a/config/locales/crowdin/js-pt.yml +++ b/config/locales/crowdin/js-pt.yml @@ -184,6 +184,8 @@ pt: new_group: 'Novo grupo' reset_to_defaults: 'Redefinir para os padrões' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmação de endereço de e-mail" confirmation_info: > diff --git a/config/locales/crowdin/js-ro.yml b/config/locales/crowdin/js-ro.yml index 75b4de0533..8deb8ed83a 100644 --- a/config/locales/crowdin/js-ro.yml +++ b/config/locales/crowdin/js-ro.yml @@ -184,6 +184,8 @@ ro: new_group: 'Grupare nouă' reset_to_defaults: 'Resetare AVATAR implicit' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmarea adresei de e-mail" confirmation_info: > diff --git a/config/locales/crowdin/js-ru.yml b/config/locales/crowdin/js-ru.yml index 157110768b..272d38cdb4 100644 --- a/config/locales/crowdin/js-ru.yml +++ b/config/locales/crowdin/js-ru.yml @@ -184,6 +184,8 @@ ru: new_group: 'Новая группа' reset_to_defaults: 'Восстановить значения по умолчанию' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Подтверждение адреса электронной почты" confirmation_info: > diff --git a/config/locales/crowdin/js-sk.yml b/config/locales/crowdin/js-sk.yml index ae5f4de7d5..a404b4c44e 100644 --- a/config/locales/crowdin/js-sk.yml +++ b/config/locales/crowdin/js-sk.yml @@ -185,6 +185,8 @@ sk: new_group: 'Nová skupina' reset_to_defaults: 'Obnoviť predvolené' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation of email address" confirmation_info: > diff --git a/config/locales/crowdin/js-sl.yml b/config/locales/crowdin/js-sl.yml index 767a31b971..2565a97928 100644 --- a/config/locales/crowdin/js-sl.yml +++ b/config/locales/crowdin/js-sl.yml @@ -184,6 +184,8 @@ sl: new_group: 'Nova skupina' reset_to_defaults: 'Ponastavi na privzete vrednosti' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Potrditev e-poštnega naslova" confirmation_info: > diff --git a/config/locales/crowdin/js-sv.yml b/config/locales/crowdin/js-sv.yml index e3be7cf48e..658c8d6b54 100644 --- a/config/locales/crowdin/js-sv.yml +++ b/config/locales/crowdin/js-sv.yml @@ -184,6 +184,8 @@ sv: new_group: 'Ny grupp' reset_to_defaults: 'Återställ standardinställningar' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Bekräftelse av e-postadress" confirmation_info: > diff --git a/config/locales/crowdin/js-tr.yml b/config/locales/crowdin/js-tr.yml index 4ba558754c..5fa3705703 100644 --- a/config/locales/crowdin/js-tr.yml +++ b/config/locales/crowdin/js-tr.yml @@ -185,6 +185,8 @@ tr: new_group: 'Yeni grup' reset_to_defaults: 'Varsayılanlara geri yükle' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "E-posta adresi doğrulama" confirmation_info: > diff --git a/config/locales/crowdin/js-uk.yml b/config/locales/crowdin/js-uk.yml index e409b447fd..f21b88a9c0 100644 --- a/config/locales/crowdin/js-uk.yml +++ b/config/locales/crowdin/js-uk.yml @@ -185,6 +185,8 @@ uk: new_group: 'Нова група' reset_to_defaults: 'Скинути на типові значення' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation of email address" confirmation_info: > diff --git a/config/locales/crowdin/js-vi.yml b/config/locales/crowdin/js-vi.yml index aa94cd1538..5536294c1f 100644 --- a/config/locales/crowdin/js-vi.yml +++ b/config/locales/crowdin/js-vi.yml @@ -184,6 +184,8 @@ vi: new_group: 'Nhóm mới' reset_to_defaults: 'Đặt lại về mặc định' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation of email address" confirmation_info: > diff --git a/config/locales/crowdin/js-zh-CN.yml b/config/locales/crowdin/js-zh-CN.yml index f2e9d81b43..404bf1bb8f 100644 --- a/config/locales/crowdin/js-zh-CN.yml +++ b/config/locales/crowdin/js-zh-CN.yml @@ -185,6 +185,8 @@ zh-CN: new_group: '新的组' reset_to_defaults: '重置为默认值' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "确认电子邮件地址" confirmation_info: > diff --git a/config/locales/crowdin/js-zh-TW.yml b/config/locales/crowdin/js-zh-TW.yml index 78b21ecee8..ef874ef08c 100644 --- a/config/locales/crowdin/js-zh-TW.yml +++ b/config/locales/crowdin/js-zh-TW.yml @@ -184,6 +184,8 @@ zh-TW: new_group: '新增群組' reset_to_defaults: '重設為預設值' enterprise: + text_reprieve_days_left: "%{days} days until end of grace period" + text_expired: "expired" trial: confirmation: "Confirmation of email address" confirmation_info: > diff --git a/config/locales/crowdin/ko.yml b/config/locales/crowdin/ko.yml index 6222c0795e..ebba8c8e5c 100644 --- a/config/locales/crowdin/ko.yml +++ b/config/locales/crowdin/ko.yml @@ -556,6 +556,7 @@ ko: invalid_url: '은(는) 올바른 URL이 아닙니다.' invalid_url_scheme: '은(는) 지원되는 프로토콜(허용: %{allowed_schemes})이 아닙니다.' less_than_or_equal_to: "은(는) %{count} 보다 작거나 같아야 합니다" + not_available: "is not available due to a system configuration." not_deletable: "- 삭제할 수 없습니다." not_current_user: "은(는) 현재 유효한 사용자가 아닙니다." not_a_date: "은(는) 유효한 날짜가 아닙니다." @@ -575,6 +576,10 @@ ko: unremovable: "- 제거할 수 없습니다." wrong_length: "너무 짧습니다 (최소 %{count} 자 이상이어야 합니다)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: '글로벌' @@ -1197,6 +1202,7 @@ ko: export: your_work_packages_export: "내 작업 패키지 내보내기" succeeded: "내보내기가 성공적으로 완료되었습니다." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2215,6 +2221,7 @@ ko: setting_apiv3_cors_origins: "API V3 CORS(크로스-원본 리소스 공유) 허용 원본" setting_apiv3_cors_origins_text_html: > CORS가 활성화된 경우, OpenProject API에 액세스하도록 허용된 원본이 있습니다.
원본 헤더의 설명서에서 예상 값 지정 방법을 확인하세요. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "이메일 배달 방법" setting_sendmail_location: "sendmail 실행 파일의 위치" setting_smtp_enable_starttls_auto: "사용 가능한 경우 자동으로 STARTTLS 사용" @@ -2329,6 +2336,13 @@ ko: passwords: "암호" session: "세션" brute_force_prevention: "자동화된 사용자 차단" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + 사용자에 대한 알림 이벤트(앱 내 알림에 대해서)가 시스템에 보관되는 일 수를 설정합니다. 이 시간보다 오래된 모든 이벤트는 삭제됩니다. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > "%{day_of_week_setting_name}"이나 "%{first_week_setting_name}" 중 하나가 설정되었다면, 프론트엔드 충돌을 방지하기 위해 나머지 하나도 설정되어야 합니다. diff --git a/config/locales/crowdin/lt.yml b/config/locales/crowdin/lt.yml index ce28ba1e54..e5a80feeba 100644 --- a/config/locales/crowdin/lt.yml +++ b/config/locales/crowdin/lt.yml @@ -556,6 +556,7 @@ lt: invalid_url: 'nėra tinkamas URL.' invalid_url_scheme: 'nėra palaikomos protokolas (leidžiamas: %{allowed_schemes}).' less_than_or_equal_to: "turi būti mažesnis arba lygus %{count}." + not_available: "yra nepasiekiamas dėl sistemos konfigūracijos" not_deletable: "negali būti pašalintas." not_current_user: "nėra dabartinis vartotojas" not_a_date: "nėra tinkama data." @@ -575,6 +576,10 @@ lt: unremovable: "negali būti pašalintas." wrong_length: "yra neteisingo ilgio (turi būti %{count} simbolių)." models: + attachment: + attributes: + content_type: + not_whitelisted: "turi reikšmę '%{value}', kurios nėra leidžiamų įkelti tipų sąraše." capability: context: global: 'Globali' @@ -1242,6 +1247,7 @@ lt: export: your_work_packages_export: "Jūsų darbo užduočių eksportas" succeeded: "Eksportas baigtas sėkmingai." + failed: "Eksportas nepavyko: %{message}" format: atom: "Atom" csv: "CSV" @@ -2089,7 +2095,7 @@ lt: permission_manage_members: "Valdyti narius" permission_manage_news: "Valdyti naujienas" permission_manage_project_activities: "Valdyti projekto veiklas" - permission_manage_public_queries: "Tvarkyti viešus rodinius" + permission_manage_public_queries: "Tvarkyti viešus vaizdus" permission_manage_repository: "Valdyti repozitoriją" permission_manage_subtasks: "Valdyti darbų paketo hierarchijas" permission_manage_versions: "Valdyti versijas" @@ -2263,6 +2269,7 @@ lt: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) leidžiami kilmės domenai" setting_apiv3_cors_origins_text_html: > Jei CORS yra įgalinta, tai yra kilmės domenai, kuriems leidžiama pasiekti OpenProject API.
Kaip nurodyti reikšmes aprašoma Origin antraštės dokumentacijoje. + setting_attachment_whitelist: "Leidžiamų failų tipų sąrašas" setting_email_delivery_method: "El. pašto pristatymo metodas" setting_sendmail_location: "Sendmail vykdomojo failo lokacija" setting_smtp_enable_starttls_auto: "Automatiškai naudoti STARTTLS, jei galima" @@ -2377,6 +2384,13 @@ lt: passwords: "Slaptažodžiai" session: "Sesija" brute_force_prevention: "Automatinis vartotojų blokavimas" + attachments: + whitelist_text_html: > + Sudarykite sąrašą leidžiamų failų praplėtimų ir/arba MIME tipų su kuriais bus leidžiama įkelti failus.
Įveskite failų praplėtimus (t.y., %{ext_example}) arba MIME tipus (t.y., %{mime_example}).
Palikite tuščią, jei norite leisti visus tipus. Leidžiama daugiau nei viena reikšmė (po vieną kiekvienoje eilutėje). + notifications: + retention_text: > + Nustatykite kiek dienų saugoti pranešimų įvykius sistemoje (tuos, kurie bus rodomi kaip programos pranešimai). Visi įvykiai, senesni nei šis laikas bus pašalinti. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > Jei vienas iš „%{day_of_week_setting_name}“ ir „%{first_week_setting_name}“ yra nustatytas, tai kitas taip pat turi būti nustatytas vengiant netikslumų vartotojo sąsajoje. diff --git a/config/locales/crowdin/nl.yml b/config/locales/crowdin/nl.yml index d926cada9e..7d8e4fd872 100644 --- a/config/locales/crowdin/nl.yml +++ b/config/locales/crowdin/nl.yml @@ -558,6 +558,7 @@ nl: invalid_url: 'is geen geldige URL.' invalid_url_scheme: 'is geen ondersteunde protocol (toegestaan: %{allowed_schemes}).' less_than_or_equal_to: "moet kleiner zijn dan of gelijk aan %{count}." + not_available: "is not available due to a system configuration." not_deletable: "cannot be deleted." not_current_user: "is not the current user." not_a_date: "is geen geldige datum." @@ -577,6 +578,10 @@ nl: unremovable: "kan niet verwijderd worden." wrong_length: "is de verkeerde lengte (dient %{count} karakters te zijn)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Globaal' @@ -1214,6 +1219,7 @@ nl: export: your_work_packages_export: "Your work packages export" succeeded: "The export has completed successfully." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2234,6 +2240,7 @@ nl: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Kies verzendmethode" setting_sendmail_location: "Locatie van het uitvoerbare sendmail" setting_smtp_enable_starttls_auto: "Gebruik STARTTLS automatisch wanneer deze beschikbaar is" @@ -2348,6 +2355,13 @@ nl: passwords: "Wachtwoorden" session: "Sessie" brute_force_prevention: "Automatische gebruiker blokkering" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/no.yml b/config/locales/crowdin/no.yml index c9a9c949fb..5e186a2690 100644 --- a/config/locales/crowdin/no.yml +++ b/config/locales/crowdin/no.yml @@ -558,6 +558,7 @@ invalid_url: 'is not a valid URL.' invalid_url_scheme: 'is not a supported protocol (allowed: %{allowed_schemes}).' less_than_or_equal_to: "must be less than or equal to %{count}." + not_available: "is not available due to a system configuration." not_deletable: "cannot be deleted." not_current_user: "is not the current user." not_a_date: "is not a valid date." @@ -577,6 +578,10 @@ unremovable: "cannot be removed." wrong_length: "is the wrong length (should be %{count} characters)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Global' @@ -1214,6 +1219,7 @@ export: your_work_packages_export: "Your work packages export" succeeded: "The export has completed successfully." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2235,6 +2241,7 @@ setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Leveringsmetode for e-post" setting_sendmail_location: "Plasseringen av sendmail kjørbar" setting_smtp_enable_starttls_auto: "Bruk STARTTLS automatisk hvis tilgjengelig" @@ -2349,6 +2356,13 @@ passwords: "Passord" session: "Sesjon" brute_force_prevention: "Automatisk blokkering av brukere" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/pl.yml b/config/locales/crowdin/pl.yml index 55a2be7db7..310acc532d 100644 --- a/config/locales/crowdin/pl.yml +++ b/config/locales/crowdin/pl.yml @@ -557,6 +557,7 @@ pl: invalid_url: 'nie jest poprawnym adresem URL.' invalid_url_scheme: 'nie jest obsługiwanym protokołem (dozwolone: %{allowed_schemes}).' less_than_or_equal_to: "musi być mniejsze niż lub równe %{count}." + not_available: "is not available due to a system configuration." not_deletable: "— nie można usunąć." not_current_user: "nie jest bieżącym użytkownikiem." not_a_date: "nie jest poprawną datą." @@ -576,6 +577,10 @@ pl: unremovable: "nie można usunąć." wrong_length: "ma nieprawidłową długość (powinno mieć %{count} znaków)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Globalna' @@ -1243,6 +1248,7 @@ pl: export: your_work_packages_export: "Eksport Twoich pakietów roboczych" succeeded: "Eksport został wykonany." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2263,6 +2269,7 @@ pl: setting_apiv3_cors_origins: "Dozwolone źródła mechanizmu Cross-Origin Resource Sharing (CORS) interfejsu API V3" setting_apiv3_cors_origins_text_html: > Jeśli mechanizm CORS jest włączony, są to źródła, których dostęp do interfejsu API OpenProject jest dozwolony.
Sprawdź w dokumentacji nagłówka origin jak należy określić oczekiwane wartości. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Metoda dostarczenia maila" setting_sendmail_location: "Lokalizacja pliku wykonywalnego sendmail" setting_smtp_enable_starttls_auto: "Automatycznie użyj STARTTLS, jeśli jest dostępny" @@ -2377,6 +2384,13 @@ pl: passwords: "Hasła" session: "Sesja" brute_force_prevention: "Automatyczne blokowanie użytkowników" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > Jeśli ustawiona jest opcja „%{day_of_week_setting_name}” albo „%{first_week_setting_name}”, druga musi zostać ustawiona w taki sposób, aby uniknąć niespójności we frontendzie. diff --git a/config/locales/crowdin/pt.yml b/config/locales/crowdin/pt.yml index 31d1f3040b..fcaecd504d 100644 --- a/config/locales/crowdin/pt.yml +++ b/config/locales/crowdin/pt.yml @@ -556,6 +556,7 @@ pt: invalid_url: 'não é um URL válido.' invalid_url_scheme: 'não é um protocolo suportado (permitidos: %{allowed_schemes}).' less_than_or_equal_to: "deve ser menor ou igual a %{count}." + not_available: "não está disponível devido a uma configuração do sistema." not_deletable: "não pode ser excluído." not_current_user: "não é o usuário atual." not_a_date: "não é uma data válida." @@ -575,6 +576,10 @@ pt: unremovable: "não pode ser removido." wrong_length: "é o tamanho errado (deve ser %{count} caracteres)." models: + attachment: + attributes: + content_type: + not_whitelisted: "está configurado para '%{value}', que não está na lista de permissões para upload. " capability: context: global: 'Global' @@ -1212,6 +1217,7 @@ pt: export: your_work_packages_export: "Exportação de seus pacotes de trabalho" succeeded: "A exportação foi completada com sucesso." + failed: "A exportação falhou: %{message}" format: atom: "Atom" csv: "CSV" @@ -2232,6 +2238,7 @@ pt: setting_apiv3_cors_origins: "Cross-Origin Resource Sharing (CORS) permitidos pela API V3" setting_apiv3_cors_origins_text_html: > Se o CORS estiver habilitado, essas são as origens que têm permissão para acessar a API OpenProject.
Por favor, verifique a documentação na header da Origin sobre como especificar os valores esperados. + setting_attachment_whitelist: "Lista de permissões de envio de anexos " setting_email_delivery_method: "Método de entrega de e-mail" setting_sendmail_location: "Localização do executável do sendmail" setting_smtp_enable_starttls_auto: "Automaticamente usar STARTTLS, se disponível" @@ -2346,6 +2353,13 @@ pt: passwords: "Senhas" session: "Sessão" brute_force_prevention: "Bloqueio automatizado de usuário" + attachments: + whitelist_text_html: > + Defina uma lista de extensões de arquivo válidas e/ou tipos MIME para arquivos carregados.
Insira as extensões de arquivo (e.x., %{ext_example}) ou tipos de míme (e.x., %{mime_example}).
Deixe em branco para permitir que qualquer tipo de arquivo seja carregado. Vários valores permitidos (uma linha para cada valor). + notifications: + retention_text: > + Defina o número de dias em que os eventos de notificação para usuários (a fonte para notificações no aplicativo) serão mantidos no sistema. Todos os eventos anteriores a esse horário serão excluídos. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > Se as opções "%{day_of_week_setting_name}" ou "%{first_week_setting_name}" forem definidas, a outra também deve ser definida, para evitar inconsistências no front-end. diff --git a/config/locales/crowdin/ro.yml b/config/locales/crowdin/ro.yml index 8d51943396..eb4f44f451 100644 --- a/config/locales/crowdin/ro.yml +++ b/config/locales/crowdin/ro.yml @@ -559,6 +559,7 @@ ro: invalid_url: 'nu este o adresa URL validă.' invalid_url_scheme: 'nu este un protocol permis (allowed: %{allowed_schemes}).' less_than_or_equal_to: "trebuie să fie mai mic sau egal cu %{count}." + not_available: "is not available due to a system configuration." not_deletable: "%s nu poate fi șters." not_current_user: "nu este utilizatorul curent." not_a_date: "Acest câmp trebuie să conțină o dată validă." @@ -578,6 +579,10 @@ ro: unremovable: "nu poate fi îndepărtată." wrong_length: "nu are lungimea corectă (ar trebui să fie de %{count} characters)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Global' @@ -1230,6 +1235,7 @@ ro: export: your_work_packages_export: "Exportul pachetelor de lucru" succeeded: "Exportul a fost finalizat cu succes." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2251,6 +2257,7 @@ ro: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) origini permise" setting_apiv3_cors_origins_text_html: > Dacă CORS este activat, acestea sunt originile cărora li se permite accesul la OpenProject API.
Vă rugăm să consultați documentația privind antetul Origin pentru a specifica valorile așteptate. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Metoda de livrare e-mail" setting_sendmail_location: "Locația executabilului \"sendmail\"" setting_smtp_enable_starttls_auto: "Utilizează automat STARTTLS dacă este disponibil" @@ -2365,6 +2372,13 @@ ro: passwords: "Parole" session: "Sesiune" brute_force_prevention: "Blocare automată a utilizatorilor" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/ru.yml b/config/locales/crowdin/ru.yml index 6c53afa8aa..2f159e534a 100644 --- a/config/locales/crowdin/ru.yml +++ b/config/locales/crowdin/ru.yml @@ -559,6 +559,7 @@ ru: invalid_url: 'не является допустимым URL-адресом.' invalid_url_scheme: 'не является поддерживаемым протоколом (разрешены следующие: %{allowed_schemes}).' less_than_or_equal_to: "должно быть меньше или равно %{count}." + not_available: "недоступно из-за конфигурации системы." not_deletable: "не может быть удален." not_current_user: "не является текущим пользователем." not_a_date: "не является допустимой датой." @@ -578,6 +579,10 @@ ru: unremovable: "не может быть удалено." wrong_length: "неправильная длина (должно быть %{count} знаков)." models: + attachment: + attributes: + content_type: + not_whitelisted: "установлено в '%{value}', который не является белым списком для загрузки." capability: context: global: 'Глобальная' @@ -1245,6 +1250,7 @@ ru: export: your_work_packages_export: "Экспорт ваших пакетов работ" succeeded: "Экспорт успешно завершен." + failed: "Экспорт не удался: %{message}" format: atom: "Atom" csv: "CSV" @@ -2265,6 +2271,7 @@ ru: setting_apiv3_cors_origins: "Разделение ресурсов (CORS) разрешено в API V3" setting_apiv3_cors_origins_text_html: > Если CORS включен, то это источники, которым разрешен доступ к OpenProject API.
Пожалуйста, проверьте документацию по происхождению о том, как указывать ожидаемые значения. + setting_attachment_whitelist: "Белый список загрузки вложений" setting_email_delivery_method: "Метод доставки электронной почты" setting_sendmail_location: "Расположение исполняемого файла sendmail" setting_smtp_enable_starttls_auto: "Автоматически использовать STARTTLS, если таковые имеются" @@ -2379,6 +2386,13 @@ ru: passwords: "Пароли" session: "Сессия" brute_force_prevention: "Автоматическое блокирование пользователя" + attachments: + whitelist_text_html: > + Определите список допустимых расширений файлов и/или mime типов для загруженных файлов.
Введите расширения файлов (например, %{ext_example}) или mime типы (e. ., %{mime_example}).
Оставьте пустым, чтобы разрешить загрузку любого типа файла. Допустимы несколько значений (одна строка для каждого значения). + notifications: + retention_text: > + Количество дней в системе уведомлений для пользователей (источник уведомлений в приложении). Все события, старше этого времени будут удалены. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > Если параметры "%{day_of_week_setting_name}" или "%{first_week_setting_name}" заданы, должны быть заданы и другие тоже во избежание несоответствий в интерфейсе. diff --git a/config/locales/crowdin/sk.yml b/config/locales/crowdin/sk.yml index 5264d980a1..592bfb578f 100644 --- a/config/locales/crowdin/sk.yml +++ b/config/locales/crowdin/sk.yml @@ -560,6 +560,7 @@ sk: invalid_url: 'nie je platnou URL adresou.' invalid_url_scheme: 'nie je podporovaný protokol (povolené: %{allowed_schemes}).' less_than_or_equal_to: "musí byť menšie alebo rovné ako %{count}." + not_available: "is not available due to a system configuration." not_deletable: "cannot be deleted." not_current_user: "is not the current user." not_a_date: "nie je platný dátum." @@ -579,6 +580,10 @@ sk: unremovable: "nie je možné zmazať." wrong_length: "má chybnú dĺžku (správne má byť %{count} znakov)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Globálna' @@ -1246,6 +1251,7 @@ sk: export: your_work_packages_export: "Your work packages export" succeeded: "Operácia bola úspešne dokončená." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2268,6 +2274,7 @@ sk: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Spôsob doručenia e-mailu" setting_sendmail_location: "Umiestnenie sendmail spustiteľného programu" setting_smtp_enable_starttls_auto: "Automaticky použiť STARTTLS, ak je k dispozícii" @@ -2382,6 +2389,13 @@ sk: passwords: "Heslá" session: "Relácie" brute_force_prevention: "Automatizované blokovanie používateľa" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/sl.yml b/config/locales/crowdin/sl.yml index 4a3fece11a..5d13b7558e 100644 --- a/config/locales/crowdin/sl.yml +++ b/config/locales/crowdin/sl.yml @@ -558,6 +558,7 @@ sl: invalid_url: 'ni veljaven URL. ' invalid_url_scheme: 'ni podprt protokol (dovoljeno:%{allowed_schemes}).' less_than_or_equal_to: "mora biti manjše ali enako %{count}. " + not_available: "is not available due to a system configuration." not_deletable: "se ne da izbrisati." not_current_user: "ni trenutni uporabnik." not_a_date: "ni veljaven datum" @@ -577,6 +578,10 @@ sl: unremovable: "ne more biti odstranjeno." wrong_length: "je napačna dolžina (mora biti %{count} znakov). " models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Globalno' @@ -1244,6 +1249,7 @@ sl: export: your_work_packages_export: "Izvoz delovnih paketov\n" succeeded: "Izvoz uspešno zaključen." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2266,6 +2272,7 @@ sl: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) dovoljen izvir" setting_apiv3_cors_origins_text_html: > To so izviri, ki jim je dovoljeno dostopati do OpenProject API, če je CORS omogočen.
Preverite dokumentacijo o glavi izvira kako navesti pričakovane vrednosti. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Način dostave po e-pošti" setting_sendmail_location: "Lokacija izvršljive pošte" setting_smtp_enable_starttls_auto: "Avtomatično uporabi STARTTLS, če je možno" @@ -2380,6 +2387,13 @@ sl: passwords: "Gesla" session: "Seja" brute_force_prevention: "Avtomatično blokiranje uporabnika" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/sv.yml b/config/locales/crowdin/sv.yml index 50c1af0aae..0e80d2421d 100644 --- a/config/locales/crowdin/sv.yml +++ b/config/locales/crowdin/sv.yml @@ -557,6 +557,7 @@ sv: invalid_url: 'är inte ett giltigt URL.' invalid_url_scheme: 'är inte ett tillåtet protokoll (tillåtna: %{allowed_schemes}).' less_than_or_equal_to: "måste vara mindre än eller lika med %{count}." + not_available: "is not available due to a system configuration." not_deletable: "cannot be deleted." not_current_user: "is not the current user." not_a_date: "är inte är ett giltigt datum." @@ -576,6 +577,10 @@ sv: unremovable: "kan inte tas bort." wrong_length: "har fel längd (måste vara %{count} tecken)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Global' @@ -1213,6 +1218,7 @@ sv: export: your_work_packages_export: "Your work packages export" succeeded: "The export has completed successfully." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2232,6 +2238,7 @@ sv: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Leveransmetod för e-post" setting_sendmail_location: "Sökväg till sendmail" setting_smtp_enable_starttls_auto: "Använd STARTTLS om det går" @@ -2346,6 +2353,13 @@ sv: passwords: "Lösenord" session: "Session" brute_force_prevention: "Automatiserad användarblockering" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/tr.yml b/config/locales/crowdin/tr.yml index 548af4d219..01e713a625 100644 --- a/config/locales/crowdin/tr.yml +++ b/config/locales/crowdin/tr.yml @@ -558,6 +558,7 @@ tr: invalid_url: 'geçerli bir adres değil.' invalid_url_scheme: 'bu protokol desteklenmiyor (izin: %{allowed_schemes}).' less_than_or_equal_to: "%{count} 'ten küçük veya bu değere eşit olmalıdır." + not_available: "Sistem yapılandırması nedeniyle kullanılamaz.\n" not_deletable: "kaldırılamadı." not_current_user: "mevcut kullanıcı değil." not_a_date: "geçerli bir tarih değil." @@ -577,6 +578,10 @@ tr: unremovable: "kaldırılamadı." wrong_length: "uzunluğu yanlıştır (%{count} karakter olmalıdır)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Küresel' @@ -1214,6 +1219,7 @@ tr: export: your_work_packages_export: "İş paketleri dışa aktarma" succeeded: "Dışa aktarma başarıyla tamamlandı." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2235,6 +2241,7 @@ tr: setting_apiv3_cors_origins: "API V3 Kaynaklar Arası Kaynak Paylaşımı (CORS) izin verilen kaynaklar" setting_apiv3_cors_origins_text_html: > CORS etkinleştirilirse, bunlar OpenProject API'ye erişmesine izin verilen kaynaklardır.
Beklenen değerlerin nasıl belirtileceğini öğrenmek için lütfen Kaynak başlığındaki Belgeleri kontrol edin. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "E-posta teslim yöntemi" setting_sendmail_location: "Çalıştırılabilir sendmail aracının yolu" setting_smtp_enable_starttls_auto: "STARTTLS varsa otomatik olarak kullan" @@ -2349,6 +2356,13 @@ tr: passwords: "Parolalar" session: "Oturum" brute_force_prevention: "Otomatik kullanıcı engelleme" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > Eğer %{day_of_week_setting_name} veya %{first_week_setting_name} seçeneklerinden biri ayarlanmışsa, ön uçta tutarsızlık yaşanmaması için diğer seçeneğin de ayarlanması gerekir. diff --git a/config/locales/crowdin/uk.yml b/config/locales/crowdin/uk.yml index f7601f55cc..757aa130ec 100644 --- a/config/locales/crowdin/uk.yml +++ b/config/locales/crowdin/uk.yml @@ -560,6 +560,7 @@ uk: invalid_url: 'не є дійсною URL-адресою.' invalid_url_scheme: 'не є підтримуваним протоколом (дозволено: %{allowed_schemes}).' less_than_or_equal_to: "має бути меншим або рівним %{count}" + not_available: "is not available due to a system configuration." not_deletable: "cannot be deleted." not_current_user: "is not the current user." not_a_date: "не є дійсною датою." @@ -579,6 +580,10 @@ uk: unremovable: "неможливо видалити." wrong_length: "неправильна довжина (повинна бути %{count} символів)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Глобальний' @@ -1246,6 +1251,7 @@ uk: export: your_work_packages_export: "Your work packages export" succeeded: "The export has completed successfully." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2269,6 +2275,7 @@ uk: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Спосіб доставки електронної пошти" setting_sendmail_location: "Розташування виконуваного файлу sendmail" setting_smtp_enable_starttls_auto: "Автоматично використовуйте STARTTLS, якщо він доступний" @@ -2383,6 +2390,13 @@ uk: passwords: "Паролі" session: "Сесія" brute_force_prevention: "Автоматизоване блокування користувачів" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/vi.yml b/config/locales/crowdin/vi.yml index ffe04e1598..d2726b174c 100644 --- a/config/locales/crowdin/vi.yml +++ b/config/locales/crowdin/vi.yml @@ -559,6 +559,7 @@ vi: invalid_url: 'không phải là một URL hợp lệ.' invalid_url_scheme: 'không phải là giao thức được hỗ trợ (được phép: %{allowed_schemes}).' less_than_or_equal_to: "phải nhỏ hơn hoặc bằng %{count}" + not_available: "is not available due to a system configuration." not_deletable: "cannot be deleted." not_current_user: "is not the current user." not_a_date: "không phải là ngày hợp lệ" @@ -578,6 +579,10 @@ vi: unremovable: "không thể gỡ bỏ." wrong_length: "độ dài không đúng (phải là %{count} ký tự)." models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: 'Global' @@ -1200,6 +1205,7 @@ vi: export: your_work_packages_export: "Your work packages export" succeeded: "The export has completed successfully." + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2220,6 +2226,7 @@ vi: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "Cách gửi email" setting_sendmail_location: "Vị trí của tệp thực thi sendmail (đường dẫn)" setting_smtp_enable_starttls_auto: "Tự động sử dụng STARTTLS nếu hiện hữu" @@ -2334,6 +2341,13 @@ vi: passwords: "Passwords" session: "Session" brute_force_prevention: "Automated user blocking" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. diff --git a/config/locales/crowdin/zh-CN.yml b/config/locales/crowdin/zh-CN.yml index 213c89227f..232dd35401 100644 --- a/config/locales/crowdin/zh-CN.yml +++ b/config/locales/crowdin/zh-CN.yml @@ -553,6 +553,7 @@ zh-CN: invalid_url: '不是有效的 URL。' invalid_url_scheme: '不是受支持的协议(允许:%{allowed_schemes})。' less_than_or_equal_to: "必须小于或等于 %{count}。" + not_available: "is not available due to a system configuration." not_deletable: "无法删除。" not_current_user: "不是当前用户。" not_a_date: "不是有效的日期。" @@ -572,6 +573,10 @@ zh-CN: unremovable: "不能删除。" wrong_length: "长度不正确 (应该是 %{count} 个字符)。" models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: '全局' @@ -1194,6 +1199,7 @@ zh-CN: export: your_work_packages_export: "您的工作包导出" succeeded: "导出已成功完成。" + failed: "导出失败: %{message}" format: atom: "原子" csv: "CSV" @@ -2210,6 +2216,7 @@ zh-CN: setting_apiv3_cors_origins: "API V3 跨源资源共享 (CORS) 允许的源" setting_apiv3_cors_origins_text_html: > 如果启用了 CORS ,这些是允许访问 OpenProject API 的源。
请查看有关“源”标题的文档,了解如何指定预期值。 + setting_attachment_whitelist: "附件上传白名单" setting_email_delivery_method: "电子邮件发送方法" setting_sendmail_location: "Sendmail 的执行位置" setting_smtp_enable_starttls_auto: "如果可用,自动使用 STARTTLS" @@ -2324,6 +2331,13 @@ zh-CN: passwords: "密码" session: "会话" brute_force_prevention: "自动化的用户阻止" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > 如果设置了选项“%{day_of_week_setting_name}”或“%{first_week_setting_name}”,也必须设置另一选项以避免前端不一致。 diff --git a/config/locales/crowdin/zh-TW.yml b/config/locales/crowdin/zh-TW.yml index 421e66c36b..95491be164 100644 --- a/config/locales/crowdin/zh-TW.yml +++ b/config/locales/crowdin/zh-TW.yml @@ -557,6 +557,7 @@ zh-TW: invalid_url: '不是有效的 URL。' invalid_url_scheme: '不是受支援的協定 (允許的協定: %{allowed_schemes})。' less_than_or_equal_to: "必須少於或者等於 %{count}。" + not_available: "is not available due to a system configuration." not_deletable: "cannot be deleted." not_current_user: "is not the current user." not_a_date: "不是有效的日期。" @@ -576,6 +577,10 @@ zh-TW: unremovable: "無法移除" wrong_length: "長度錯誤 (應該要有 %{count} 個字元)" models: + attachment: + attributes: + content_type: + not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." capability: context: global: '全域設定' @@ -1198,6 +1203,7 @@ zh-TW: export: your_work_packages_export: "輸出工作列表" succeeded: "輸出完成" + failed: "The export has failed: %{message}" format: atom: "Atom" csv: "CSV" @@ -2217,6 +2223,7 @@ zh-TW: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. + setting_attachment_whitelist: "Attachment upload whitelist" setting_email_delivery_method: "電子郵件傳遞方法" setting_sendmail_location: "Sendmail 的執行位置" setting_smtp_enable_starttls_auto: "如果可用將自動使用 STARTTLS" @@ -2331,6 +2338,13 @@ zh-TW: passwords: "密碼" session: "Session" brute_force_prevention: "自動禁止使用者" + attachments: + whitelist_text_html: > + Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + notifications: + retention_text: > + Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > If either options "%{day_of_week_setting_name}" or "%{first_week_setting_name}" are set, the other has to be set as well to avoid inconsistencies in the frontend. From 3a4687ff9f6e7750f678ae82eff223466b23b040 Mon Sep 17 00:00:00 2001 From: Wieland Lindenthal Date: Wed, 22 Sep 2021 15:30:13 +0200 Subject: [PATCH 61/68] [#38954] IFC upload not working since attachment whitelisting * Saving the IFC file (at least during local upload): Validation of permissions for creating attachments fail as the project on which to check for permissions is missing * During conversion saving XKT file fails as it not an uploaded file and thus method `original_filename` is not present https://community.openproject.org/work_packages/38954 --- modules/bim/app/models/bim/ifc_models/ifc_model.rb | 4 ++-- .../bim/app/services/bim/ifc_models/set_attributes_service.rb | 1 + .../features/{bcf => ifc_models}/direct_ifc_upload_spec.rb | 0 3 files changed, 3 insertions(+), 2 deletions(-) rename modules/bim/spec/features/{bcf => ifc_models}/direct_ifc_upload_spec.rb (100%) diff --git a/modules/bim/app/models/bim/ifc_models/ifc_model.rb b/modules/bim/app/models/bim/ifc_models/ifc_model.rb index 940acb76fe..6499e433d0 100644 --- a/modules/bim/app/models/bim/ifc_models/ifc_model.rb +++ b/modules/bim/app/models/bim/ifc_models/ifc_model.rb @@ -13,7 +13,7 @@ module Bim scope :defaults, -> { where(is_default: true) } - %i(ifc xkt metadata).each do |name| + %i(ifc xkt).each do |name| define_method "#{name}_attachment" do get_attached_type(name) end @@ -27,7 +27,7 @@ module Bim delete_attachment name call = ::Attachments::CreateService .bypass_whitelist(user: User.current) - .call(file: file, container: self, filename: file.original_filename, description: name) + .call(file: file, container: self, filename: File.basename(file.path), description: name) call.on_failure { Rails.logger.error "Failed to add #{name} attachment: #{call.message}" } end diff --git a/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb b/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb index b703ceee16..a28c1b2934 100644 --- a/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb +++ b/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb @@ -34,6 +34,7 @@ module Bim protected def set_attributes(params) + model.project = params[:project] set_ifc_attachment(params.delete(:ifc_attachment)) super diff --git a/modules/bim/spec/features/bcf/direct_ifc_upload_spec.rb b/modules/bim/spec/features/ifc_models/direct_ifc_upload_spec.rb similarity index 100% rename from modules/bim/spec/features/bcf/direct_ifc_upload_spec.rb rename to modules/bim/spec/features/ifc_models/direct_ifc_upload_spec.rb From 8e6d6beca905e9dd6bda235752ebf91938392705 Mon Sep 17 00:00:00 2001 From: OpenProject Actions CI Date: Thu, 23 Sep 2021 03:02:36 +0000 Subject: [PATCH 62/68] update locales from crowdin [ci skip] --- config/locales/crowdin/js-ru.yml | 4 ++-- config/locales/crowdin/ru.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/locales/crowdin/js-ru.yml b/config/locales/crowdin/js-ru.yml index 272d38cdb4..012bf76145 100644 --- a/config/locales/crowdin/js-ru.yml +++ b/config/locales/crowdin/js-ru.yml @@ -184,8 +184,8 @@ ru: new_group: 'Новая группа' reset_to_defaults: 'Восстановить значения по умолчанию' enterprise: - text_reprieve_days_left: "%{days} days until end of grace period" - text_expired: "expired" + text_reprieve_days_left: "%{days} дней до окончания льготного периода" + text_expired: "просрочено" trial: confirmation: "Подтверждение адреса электронной почты" confirmation_info: > diff --git a/config/locales/crowdin/ru.yml b/config/locales/crowdin/ru.yml index 2f159e534a..d08181521f 100644 --- a/config/locales/crowdin/ru.yml +++ b/config/locales/crowdin/ru.yml @@ -2392,7 +2392,7 @@ ru: notifications: retention_text: > Количество дней в системе уведомлений для пользователей (источник уведомлений в приложении). Все события, старше этого времени будут удалены. - delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." + delay_minutes_explanation: "Отправка электронной почты может быть отложена для разрешения пользователям с настроенными в приложении уведомлениями подтвердить уведомление внутри приложения перед отправкой почты. Пользователи, которые прочитали уведомление в приложении, не будут получать по электронной почте уже прочитанное уведомление." display: first_date_of_week_and_year_set: > Если параметры "%{day_of_week_setting_name}" или "%{first_week_setting_name}" заданы, должны быть заданы и другие тоже во избежание несоответствий в интерфейсе. From 8d2ce8b893319850942e091edf0883e2b2be6959 Mon Sep 17 00:00:00 2001 From: OpenProject Actions CI Date: Fri, 24 Sep 2021 03:11:53 +0000 Subject: [PATCH 63/68] update locales from crowdin [ci skip] --- config/locales/crowdin/js-lt.yml | 4 ++-- config/locales/crowdin/lt.yml | 2 +- modules/backlogs/config/locales/crowdin/lt.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/locales/crowdin/js-lt.yml b/config/locales/crowdin/js-lt.yml index 82a869c31f..f35f58c0c7 100644 --- a/config/locales/crowdin/js-lt.yml +++ b/config/locales/crowdin/js-lt.yml @@ -185,8 +185,8 @@ lt: new_group: 'Nauja grupė' reset_to_defaults: 'Atkurti į numatytuosius' enterprise: - text_reprieve_days_left: "%{days} days until end of grace period" - text_expired: "expired" + text_reprieve_days_left: "%{days} dienos iki lengvatos pabaigos" + text_expired: "nebegalioja" trial: confirmation: "Patvirtinti el. pašto adresą" confirmation_info: > diff --git a/config/locales/crowdin/lt.yml b/config/locales/crowdin/lt.yml index e5a80feeba..74267e53c6 100644 --- a/config/locales/crowdin/lt.yml +++ b/config/locales/crowdin/lt.yml @@ -2390,7 +2390,7 @@ lt: notifications: retention_text: > Nustatykite kiek dienų saugoti pranešimų įvykius sistemoje (tuos, kurie bus rodomi kaip programos pranešimai). Visi įvykiai, senesni nei šis laikas bus pašalinti. - delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." + delay_minutes_explanation: "El.laiškų siuntimas gali būti užlaikytas, kad būtų leista naudotojams, kurie yra susikonfigūravę gauti pranešimus tiesiai programoje, patvirtinti iki to, kol bus išsiųstas laiškas. Naudotojai, kurie perskaitė pranešimus programoje, negaus tų laiškų, kurių pranešimus jau bus perskaitę." display: first_date_of_week_and_year_set: > Jei vienas iš „%{day_of_week_setting_name}“ ir „%{first_week_setting_name}“ yra nustatytas, tai kitas taip pat turi būti nustatytas vengiant netikslumų vartotojo sąsajoje. diff --git a/modules/backlogs/config/locales/crowdin/lt.yml b/modules/backlogs/config/locales/crowdin/lt.yml index 5be8af2fec..1f0b045a67 100644 --- a/modules/backlogs/config/locales/crowdin/lt.yml +++ b/modules/backlogs/config/locales/crowdin/lt.yml @@ -90,7 +90,7 @@ lt: backlogs_sprint_unsized: "Projekto aktyvūs ir neseniai uždaryti sprintai turi istorijų, kurių dydis nėra įvertintas" backlogs_sprints: "Sprintai" backlogs_story: "Istorija" - backlogs_story_type: "Istorijos tipas" + backlogs_story_type: "Istorijų tipai" backlogs_task: "Užduotis" backlogs_task_type: "Užduoties tipas" backlogs_velocity_missing: "Šiam projektui negalima paskaičiuoti greičio" From e417d1c10e245c5d86181b0d291f2d12003edd04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Fri, 24 Sep 2021 21:01:51 +0200 Subject: [PATCH 64/68] Don't override the project if not present in params --- .../bim/app/services/bim/ifc_models/set_attributes_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb b/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb index a28c1b2934..0120d833e9 100644 --- a/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb +++ b/modules/bim/app/services/bim/ifc_models/set_attributes_service.rb @@ -34,7 +34,7 @@ module Bim protected def set_attributes(params) - model.project = params[:project] + model.project = params[:project] if params.key?(:project) set_ifc_attachment(params.delete(:ifc_attachment)) super From af5e2e84942f81186aab461d7a8b5a2bcfdc8471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Fri, 24 Sep 2021 21:46:13 +0200 Subject: [PATCH 65/68] Add spec for regular uploads as well --- .../ifc_models/direct_ifc_upload_spec.rb | 28 +++------------ .../ifc_models/ifc_upload_shared_examples.rb | 26 ++++++++++++++ .../ifc_models/regular_ifc_upload_spec.rb | 36 +++++++++++++++++++ 3 files changed, 66 insertions(+), 24 deletions(-) create mode 100644 modules/bim/spec/features/ifc_models/ifc_upload_shared_examples.rb create mode 100644 modules/bim/spec/features/ifc_models/regular_ifc_upload_spec.rb diff --git a/modules/bim/spec/features/ifc_models/direct_ifc_upload_spec.rb b/modules/bim/spec/features/ifc_models/direct_ifc_upload_spec.rb index 9c6ec13a03..944bf93ac5 100644 --- a/modules/bim/spec/features/ifc_models/direct_ifc_upload_spec.rb +++ b/modules/bim/spec/features/ifc_models/direct_ifc_upload_spec.rb @@ -27,31 +27,11 @@ #++ require 'spec_helper' +require_relative './ifc_upload_shared_examples' describe 'direct IFC upload', type: :feature, js: true, with_direct_uploads: :redirect, with_config: { edition: 'bim' } do - let(:user) { FactoryBot.create :admin } - let(:project) { FactoryBot.create :project, enabled_module_names: %i[bim] } - let(:ifc_fixture) { ::UploadedFile.load_from('modules/bim/spec/fixtures/files/minimal.ifc') } - - before do - login_as user - - allow_any_instance_of(Bim::IfcModels::BaseContract).to receive(:ifc_attachment_is_ifc).and_return true - end - - it 'should work' do - visit new_bcf_project_ifc_model_path(project_id: project.identifier) - - page.attach_file("file", ifc_fixture.path, visible: :all) - - click_on "Create" - - expect(page).to have_content("Upload succeeded") - - expect(Attachment.count).to eq 1 - expect(Attachment.first[:file]).to eq 'model.ifc' - - expect(Bim::IfcModels::IfcModel.count).to eq 1 - expect(Bim::IfcModels::IfcModel.first.title).to eq "minimal.ifc" + it_behaves_like 'can upload an IFC file' do + # with direct upload, we don't get the model name + let(:model_name) { 'model.ifc' } end end diff --git a/modules/bim/spec/features/ifc_models/ifc_upload_shared_examples.rb b/modules/bim/spec/features/ifc_models/ifc_upload_shared_examples.rb new file mode 100644 index 0000000000..b77730accf --- /dev/null +++ b/modules/bim/spec/features/ifc_models/ifc_upload_shared_examples.rb @@ -0,0 +1,26 @@ +shared_examples 'can upload an IFC file' do + let(:user) { FactoryBot.create :admin } + let(:project) { FactoryBot.create :project, enabled_module_names: %i[bim] } + let(:ifc_fixture) { ::UploadedFile.load_from('modules/bim/spec/fixtures/files/minimal.ifc') } + + before do + login_as user + + allow_any_instance_of(Bim::IfcModels::BaseContract).to receive(:ifc_attachment_is_ifc).and_return true + end + + it 'should allow uploading an IFC file' do + visit new_bcf_project_ifc_model_path(project_id: project.identifier) + + page.attach_file("file", ifc_fixture.path, visible: :all) + + click_on "Create" + + expect(page).to have_content("Upload succeeded") + + expect(Attachment.count).to eq 1 + expect(Attachment.first[:file]).to eq model_name + + expect(Bim::IfcModels::IfcModel.count).to eq 1 + end +end diff --git a/modules/bim/spec/features/ifc_models/regular_ifc_upload_spec.rb b/modules/bim/spec/features/ifc_models/regular_ifc_upload_spec.rb new file mode 100644 index 0000000000..5db756975b --- /dev/null +++ b/modules/bim/spec/features/ifc_models/regular_ifc_upload_spec.rb @@ -0,0 +1,36 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require_relative './ifc_upload_shared_examples' + +describe 'IFC upload', type: :feature, js: true, with_config: { edition: 'bim' } do + it_behaves_like 'can upload an IFC file' do + let(:model_name) { 'minimal.ifc' } + end +end From 97f1713545f97ebab36c3a27c44183bec3f22609 Mon Sep 17 00:00:00 2001 From: OpenProject Actions CI Date: Sun, 26 Sep 2021 03:12:34 +0000 Subject: [PATCH 66/68] update locales from crowdin [ci skip] --- config/locales/crowdin/es.yml | 2 +- config/locales/crowdin/zh-TW.yml | 12 ++++++------ modules/backlogs/config/locales/crowdin/lt.yml | 2 +- modules/bim/config/locales/crowdin/js-es.yml | 4 ++-- modules/bim/config/locales/crowdin/lt.yml | 2 +- .../config/locales/crowdin/js-ja.yml | 6 +++--- modules/ldap_groups/config/locales/crowdin/lt.yml | 6 +++--- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/config/locales/crowdin/es.yml b/config/locales/crowdin/es.yml index 58070d098f..0037c52f53 100644 --- a/config/locales/crowdin/es.yml +++ b/config/locales/crowdin/es.yml @@ -555,7 +555,7 @@ es: invalid_url: 'no es una URL válida.' invalid_url_scheme: 'no es un protocolo admitido (permitidos: %{allowed_schemes}).' less_than_or_equal_to: "debe ser menor o igual a %{count}." - not_available: "is not available due to a system configuration." + not_available: "no está disponible debido a una configuración del sistema." not_deletable: "no se puede eliminar." not_current_user: "no es el usuario actual." not_a_date: "no es una fecha válida." diff --git a/config/locales/crowdin/zh-TW.yml b/config/locales/crowdin/zh-TW.yml index 95491be164..98f0b4aa81 100644 --- a/config/locales/crowdin/zh-TW.yml +++ b/config/locales/crowdin/zh-TW.yml @@ -557,7 +557,7 @@ zh-TW: invalid_url: '不是有效的 URL。' invalid_url_scheme: '不是受支援的協定 (允許的協定: %{allowed_schemes})。' less_than_or_equal_to: "必須少於或者等於 %{count}。" - not_available: "is not available due to a system configuration." + not_available: "由於系統配置所以不可用" not_deletable: "cannot be deleted." not_current_user: "is not the current user." not_a_date: "不是有效的日期。" @@ -580,7 +580,7 @@ zh-TW: attachment: attributes: content_type: - not_whitelisted: "is set to '%{value}', which is not whitelisted for uploading." + not_whitelisted: "設置為 '%{value}',未列入上傳白名單" capability: context: global: '全域設定' @@ -1203,7 +1203,7 @@ zh-TW: export: your_work_packages_export: "輸出工作列表" succeeded: "輸出完成" - failed: "The export has failed: %{message}" + failed: "匯出失敗:%{message}" format: atom: "Atom" csv: "CSV" @@ -2223,7 +2223,7 @@ zh-TW: setting_apiv3_cors_origins: "API V3 Cross-Origin Resource Sharing (CORS) allowed origins" setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
Please check the Documentation on the Origin header on how to specify the expected values. - setting_attachment_whitelist: "Attachment upload whitelist" + setting_attachment_whitelist: "附件上傳白名單" setting_email_delivery_method: "電子郵件傳遞方法" setting_sendmail_location: "Sendmail 的執行位置" setting_smtp_enable_starttls_auto: "如果可用將自動使用 STARTTLS" @@ -2340,10 +2340,10 @@ zh-TW: brute_force_prevention: "自動禁止使用者" attachments: whitelist_text_html: > - Define a list of valid file extensions and/or mime types for uploaded files.
Enter file extensions (e.g., %{ext_example}) or mime types (e.g., %{mime_example}).
Leave empty to allow any file type to be uploaded. Multiple values allowed (one line for each value). + 為上傳的文件定義有效文件副檔名和/或 MIME 類型的列表。
輸入文件副檔名(例如 %{ext_example})或 MIME 類型(例如 %{mime_example})。
留空以允許上傳任何文件類型。允許多個值(每個值一行)。 notifications: retention_text: > - Set the number of days notification events for users (the source for in-app notifications) will be kept in the system. Any events older than this time will be deleted. + 設定用戶的通知事件(應用程式內通知的來源)將保留在系統中的天數。任何早於此時間的事件都將被刪除 delay_minutes_explanation: "Email sending can be delayed to allow users with configured in app notification to confirm the notification within the application before a mail is sent out. Users who read a notification within the application will not receive an email for the already read notification." display: first_date_of_week_and_year_set: > diff --git a/modules/backlogs/config/locales/crowdin/lt.yml b/modules/backlogs/config/locales/crowdin/lt.yml index 1f0b045a67..65381d8d68 100644 --- a/modules/backlogs/config/locales/crowdin/lt.yml +++ b/modules/backlogs/config/locales/crowdin/lt.yml @@ -149,7 +149,7 @@ lt: rb_label_copy_tasks_none: "Joks" rb_label_copy_tasks_open: "Atidaryti" rb_label_link_to_original: "Įtraukti nuorodą į originalią istoriją" - remaining_hours: "likę valandos" + remaining_hours: "likusios valandos" required_burn_rate_hours: "reikalingas degimo tempas (valandos)" required_burn_rate_points: "reikalingas degimo tempas (taškai)" todo_work_package_description: "%{summary}: %{url}\n%{description}" diff --git a/modules/bim/config/locales/crowdin/js-es.yml b/modules/bim/config/locales/crowdin/js-es.yml index 1a19331795..a420927b0b 100644 --- a/modules/bim/config/locales/crowdin/js-es.yml +++ b/modules/bim/config/locales/crowdin/js-es.yml @@ -12,8 +12,8 @@ es: show_viewpoint: 'Mostrar área de visualización' delete_viewpoint: 'Eliminar área de visualización' management: 'Administración de BCF' - refresh: 'Refresh' - refresh_work_package: 'Refresh work package' + refresh: 'Refrescar' + refresh_work_package: 'Refrescar paquete de trabajo' ifc_models: empty_warning: "Este proyecto aún no tiene ningún modelo IFC." use_this_link_to_manage: "Use este enlace para cargar y administrar sus modelos IFC" diff --git a/modules/bim/config/locales/crowdin/lt.yml b/modules/bim/config/locales/crowdin/lt.yml index 7dc4aba76c..fd678d9ac7 100644 --- a/modules/bim/config/locales/crowdin/lt.yml +++ b/modules/bim/config/locales/crowdin/lt.yml @@ -24,7 +24,7 @@ lt: import_update_comment: '(Atnaujinta BCF importo metu)' import_failed: 'Negaliu importuoti BCF failo: %{error}' import_failed_unsupported_bcf_version: 'Nepavyko perskaityti BCF failo: Nepalaikoma BCF versija. Įsitikinkite, kad versija yra bent %{minimal_version} ar aukštesnė.' - import_successful: 'Importuota %{count} BCF trūkumų' + import_successful: 'Importuota %{count} BCF problemų' import_canceled: 'BCF-XML importas nutrauktas.' type_not_active: "Trūkumo tipas nėra įjungtas šiam projektui." import: diff --git a/modules/github_integration/config/locales/crowdin/js-ja.yml b/modules/github_integration/config/locales/crowdin/js-ja.yml index 3fb16daad9..6665617773 100644 --- a/modules/github_integration/config/locales/crowdin/js-ja.yml +++ b/modules/github_integration/config/locales/crowdin/js-ja.yml @@ -28,12 +28,12 @@ ja: title: "プルリクエスト" copy_menu: label: Git snippets - description: Copy git snippets to clipboard + description: git スニペットをクリップボードにコピー git_actions: branch_name: ブランチ名 commit_message: コミットメッセージ - cmd: Create branch with empty commit - title: Quick snippets for Git + cmd: 空のコミットでブランチを作成 + title: Git 用のクイックスニペットです。 copy_success: '✅ コピーしました!' copy_error: '❌ コピーに失敗しました!' tab_prs: diff --git a/modules/ldap_groups/config/locales/crowdin/lt.yml b/modules/ldap_groups/config/locales/crowdin/lt.yml index 0aa2dc38d9..5d9bdca24a 100644 --- a/modules/ldap_groups/config/locales/crowdin/lt.yml +++ b/modules/ldap_groups/config/locales/crowdin/lt.yml @@ -53,12 +53,12 @@ lt: verification: "Įveskite grupės pavadinimą %{name}, kad patvirtintumėte naikinimą." help_text_html: | Šis modulis leidžia jums nustatyti sinchronizavimą tarp LDAP ir OpenProject grupių. - Jis remiasi tuo, kad LDAP groupės turi naudoti groupOfNames / memberOf atributų rinkinį, kad dirbtų su OpenProject. + Jis remiasi tuo, kad LDAP grupės turi naudoti groupOfNames / memberOf atributų rinkinį, kad dirbtų su OpenProject.
Grupės yra sinchronizuojamos kas valandą naudojant cron užduotį. Prašome pasiskaityti dokumentaciją šia tema (angliškai). - no_results: 'Nerasta sinchronizacijos grupių.' - no_members: 'Grupė dar neturi sinchronizacijos narių.' + no_results: 'Nerasta sinchronizuotų grupių.' + no_members: 'Grupė dar neturi sinchronizuotų narių.' plural: 'Sinchronizuotos LDAP grupės' singular: 'Sinchronizuota LDAP grupė' form: From d166180a30fdf0af49509101899ace487740212e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 27 Sep 2021 13:27:55 +0200 Subject: [PATCH 67/68] Use original_filename if present --- modules/bim/app/models/bim/ifc_models/ifc_model.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/bim/app/models/bim/ifc_models/ifc_model.rb b/modules/bim/app/models/bim/ifc_models/ifc_model.rb index 6499e433d0..801cf04de6 100644 --- a/modules/bim/app/models/bim/ifc_models/ifc_model.rb +++ b/modules/bim/app/models/bim/ifc_models/ifc_model.rb @@ -25,9 +25,10 @@ module Bim end delete_attachment name + filename = file.respond_to?(:original_filename) ? file.original_filename : File.basename(file.path) call = ::Attachments::CreateService .bypass_whitelist(user: User.current) - .call(file: file, container: self, filename: File.basename(file.path), description: name) + .call(file: file, container: self, filename: filename, description: name) call.on_failure { Rails.logger.error "Failed to add #{name} attachment: #{call.message}" } end From 9d45ce65f376703d920088a528867670ea457150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 27 Sep 2021 13:28:02 +0200 Subject: [PATCH 68/68] Run IFC conversion as a system user --- .../app/workers/bim/ifc_models/ifc_conversion_job.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/bim/app/workers/bim/ifc_models/ifc_conversion_job.rb b/modules/bim/app/workers/bim/ifc_models/ifc_conversion_job.rb index c983bb6379..1366f79242 100644 --- a/modules/bim/app/workers/bim/ifc_models/ifc_conversion_job.rb +++ b/modules/bim/app/workers/bim/ifc_models/ifc_conversion_job.rb @@ -6,11 +6,13 @@ module Bim ## # Run the conversion of IFC to def perform(ifc_model) - result = ViewConverterService.new(ifc_model).call + User.system.run_given do + result = ViewConverterService.new(ifc_model).call - unless result.success? - errors = result.errors.full_messages.join(". ") - Rails.logger.error "Failed to convert IFC model #{ifc_model.inspect}: #{errors}" + unless result.success? + errors = result.errors.full_messages.join(". ") + Rails.logger.error "Failed to convert IFC model #{ifc_model.inspect}: #{errors}" + end end end end