Release OpenProject 11.1.3

release/11.2 v11.1.3
ulferts 4 years ago
commit 5d2dcf0dd1
No known key found for this signature in database
GPG Key ID: A205708DE1284017
  1. 2
      .github/workflows/docker.yml
  2. 2
      .ruby-version
  3. 2
      .travis.yml
  4. 4
      Gemfile
  5. 106
      Gemfile.lock
  6. 6
      app/helpers/versions_helper.rb
  7. 2
      app/models/group.rb
  8. 2
      app/models/queries/work_packages/filter/project_filter.rb
  9. 4
      app/models/user.rb
  10. 2
      app/models/work_package/exporter/csv.rb
  11. 99
      app/uploaders/direct_fog_uploader.rb
  12. 13
      app/uploaders/fog_file_uploader.rb
  13. 1
      app/views/versions/_roadmap_filter.html.erb
  14. 5
      app/views/versions/_roadmap_version_links.html.erb
  15. 2
      app/views/versions/index.html.erb
  16. 4
      config/locales/crowdin/js-tr.yml
  17. 13
      db/migrate/20210127134438_alter_user_attributes_max_length.rb
  18. 2
      docker/dev/backend/Dockerfile
  19. 2
      docker/prod/Dockerfile
  20. 8
      docs/api/apiv3/endpoints/attachments.apib
  21. 28
      docs/api/apiv3/endpoints/members.apib
  22. 1
      docs/api/apiv3/forms.apib
  23. 4
      docs/api/apiv3/index.apib
  24. 8
      docs/development/development-environment-osx/README.md
  25. 8
      docs/development/development-environment-ubuntu/README.md
  26. 11
      docs/installation-and-operations/changing-to-bim-edition/README.md
  27. 16
      docs/installation-and-operations/configuration/README.md
  28. 5
      docs/installation-and-operations/configuration/ssl/README.md
  29. 6
      docs/installation-and-operations/installation/manual/README.md
  30. 28
      docs/release-notes/11-1-3/README.md
  31. 7
      docs/release-notes/README.md
  32. 4
      docs/system-admin-guide/authentication/ldap-authentication/ldap-group-synchronization/README.md
  33. 27
      docs/system-admin-guide/users-permissions/users/README.md
  34. 13
      docs/user-guide/repository/README.md
  35. 4
      docs/user-guide/work-packages/work-package-table-configuration/README.md
  36. 19
      frontend/src/app/components/modals/export-modal/wp-table-export.modal.ts
  37. 58
      frontend/src/app/modules/fields/edit/field-types/multi-select-edit-field.component.html
  38. 1
      frontend/src/app/modules/job-status/job-status-modal/job-status.modal.html
  39. 5
      lib/api/v3/queries/schemas/project_filter_dependency_representer.rb
  40. 1
      lib/open_project/configuration.rb
  41. 2
      lib/open_project/version.rb
  42. 4
      modules/bim/config/locales/crowdin/uk.yml
  43. 10
      modules/costs/spec/requests/api/attachments/attachments_by_budget_resource_spec.rb
  44. 10
      modules/documents/spec/requests/api/v3/attachments/attachments_by_documents_resource_spec.rb
  45. 1
      modules/ldap_groups/app/models/ldap_groups/synchronized_group.rb
  46. 2
      modules/ldap_groups/lib/open_project/ldap_groups/synchronize_filter.rb
  47. 27
      spec/features/work_packages/export_spec.rb
  48. 18
      spec/helpers/versions_helper_spec.rb
  49. 3
      spec/lib/api/v3/queries/schemas/project_filter_dependency_representer_spec.rb
  50. 2
      spec/models/attachment_spec.rb
  51. 23
      spec/models/group_spec.rb
  52. 53
      spec/models/queries/work_packages/filter/project_filter_instance_spec.rb
  53. 8
      spec/models/queries/work_packages/filter/project_filter_spec.rb
  54. 28
      spec/models/user_spec.rb
  55. 39
      spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb
  56. 4
      spec/requests/api/v3/attachments_spec.rb
  57. 7
      spec_legacy/unit/mail_handler_spec.rb

@ -12,6 +12,8 @@ jobs:
# restrict this job to base repo for now # restrict this job to base repo for now
if: github.repository == 'opf/openproject' if: github.repository == 'opf/openproject'
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
INPUT_BUILDOPTIONS: --pull
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Prepare docker files - name: Prepare docker files

@ -1 +1 @@
2.7.1 2.7.2

@ -29,7 +29,7 @@
language: ruby language: ruby
rvm: rvm:
- 2.7.1 - 2.7.2
sudo: required sudo: required
dist: xenial dist: xenial

@ -28,13 +28,13 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '~> 2.7.1' ruby '~> 2.7.2'
gem 'actionpack-xml_parser', '~> 2.0.0' gem 'actionpack-xml_parser', '~> 2.0.0'
gem 'activemodel-serializers-xml', '~> 1.0.1' gem 'activemodel-serializers-xml', '~> 1.0.1'
gem 'activerecord-import', '~> 1.0.2' gem 'activerecord-import', '~> 1.0.2'
gem 'activerecord-session_store', '~> 1.1.0' gem 'activerecord-session_store', '~> 1.1.0'
gem 'rails', '~> 6.0.3.2' gem 'rails', '~> 6.0.3.5'
gem 'responders', '~> 3.0' gem 'responders', '~> 3.0'
gem 'rdoc', '>= 2.4.2' gem 'rdoc', '>= 2.4.2'

@ -183,26 +183,26 @@ GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
Ascii85 (1.0.3) Ascii85 (1.0.3)
actioncable (6.0.3.4) actioncable (6.0.3.5)
actionpack (= 6.0.3.4) actionpack (= 6.0.3.5)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.0.3.4) actionmailbox (6.0.3.5)
actionpack (= 6.0.3.4) actionpack (= 6.0.3.5)
activejob (= 6.0.3.4) activejob (= 6.0.3.5)
activerecord (= 6.0.3.4) activerecord (= 6.0.3.5)
activestorage (= 6.0.3.4) activestorage (= 6.0.3.5)
activesupport (= 6.0.3.4) activesupport (= 6.0.3.5)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.0.3.4) actionmailer (6.0.3.5)
actionpack (= 6.0.3.4) actionpack (= 6.0.3.5)
actionview (= 6.0.3.4) actionview (= 6.0.3.5)
activejob (= 6.0.3.4) activejob (= 6.0.3.5)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.0.3.4) actionpack (6.0.3.5)
actionview (= 6.0.3.4) actionview (= 6.0.3.5)
activesupport (= 6.0.3.4) activesupport (= 6.0.3.5)
rack (~> 2.0, >= 2.0.8) rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@ -210,30 +210,30 @@ GEM
actionpack-xml_parser (2.0.1) actionpack-xml_parser (2.0.1)
actionpack (>= 5.0) actionpack (>= 5.0)
railties (>= 5.0) railties (>= 5.0)
actiontext (6.0.3.4) actiontext (6.0.3.5)
actionpack (= 6.0.3.4) actionpack (= 6.0.3.5)
activerecord (= 6.0.3.4) activerecord (= 6.0.3.5)
activestorage (= 6.0.3.4) activestorage (= 6.0.3.5)
activesupport (= 6.0.3.4) activesupport (= 6.0.3.5)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.0.3.4) actionview (6.0.3.5)
activesupport (= 6.0.3.4) activesupport (= 6.0.3.5)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.0.3.4) activejob (6.0.3.5)
activesupport (= 6.0.3.4) activesupport (= 6.0.3.5)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.0.3.4) activemodel (6.0.3.5)
activesupport (= 6.0.3.4) activesupport (= 6.0.3.5)
activemodel-serializers-xml (1.0.2) activemodel-serializers-xml (1.0.2)
activemodel (> 5.x) activemodel (> 5.x)
activesupport (> 5.x) activesupport (> 5.x)
builder (~> 3.1) builder (~> 3.1)
activerecord (6.0.3.4) activerecord (6.0.3.5)
activemodel (= 6.0.3.4) activemodel (= 6.0.3.5)
activesupport (= 6.0.3.4) activesupport (= 6.0.3.5)
activerecord-import (1.0.7) activerecord-import (1.0.7)
activerecord (>= 3.2) activerecord (>= 3.2)
activerecord-nulldb-adapter (0.5.1) activerecord-nulldb-adapter (0.5.1)
@ -244,12 +244,12 @@ GEM
multi_json (~> 1.11, >= 1.11.2) multi_json (~> 1.11, >= 1.11.2)
rack (>= 1.5.2, < 3) rack (>= 1.5.2, < 3)
railties (>= 4.0) railties (>= 4.0)
activestorage (6.0.3.4) activestorage (6.0.3.5)
actionpack (= 6.0.3.4) actionpack (= 6.0.3.5)
activejob (= 6.0.3.4) activejob (= 6.0.3.5)
activerecord (= 6.0.3.4) activerecord (= 6.0.3.5)
marcel (~> 0.3.1) marcel (~> 0.3.1)
activesupport (6.0.3.4) activesupport (6.0.3.5)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
minitest (~> 5.1) minitest (~> 5.1)
@ -694,20 +694,20 @@ GEM
rack_session_access (0.2.0) rack_session_access (0.2.0)
builder (>= 2.0.0) builder (>= 2.0.0)
rack (>= 1.0.0) rack (>= 1.0.0)
rails (6.0.3.4) rails (6.0.3.5)
actioncable (= 6.0.3.4) actioncable (= 6.0.3.5)
actionmailbox (= 6.0.3.4) actionmailbox (= 6.0.3.5)
actionmailer (= 6.0.3.4) actionmailer (= 6.0.3.5)
actionpack (= 6.0.3.4) actionpack (= 6.0.3.5)
actiontext (= 6.0.3.4) actiontext (= 6.0.3.5)
actionview (= 6.0.3.4) actionview (= 6.0.3.5)
activejob (= 6.0.3.4) activejob (= 6.0.3.5)
activemodel (= 6.0.3.4) activemodel (= 6.0.3.5)
activerecord (= 6.0.3.4) activerecord (= 6.0.3.5)
activestorage (= 6.0.3.4) activestorage (= 6.0.3.5)
activesupport (= 6.0.3.4) activesupport (= 6.0.3.5)
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 6.0.3.4) railties (= 6.0.3.5)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
@ -721,15 +721,15 @@ GEM
rails-i18n (6.0.0) rails-i18n (6.0.0)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7) railties (>= 6.0.0, < 7)
railties (6.0.3.4) railties (6.0.3.5)
actionpack (= 6.0.3.4) actionpack (= 6.0.3.5)
activesupport (= 6.0.3.4) activesupport (= 6.0.3.5)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0) thor (>= 0.20.3, < 2.0)
rainbow (3.0.0) rainbow (3.0.0)
raindrops (0.19.1) raindrops (0.19.1)
rake (13.0.1) rake (13.0.3)
rb-fsevent (0.10.4) rb-fsevent (0.10.4)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
@ -1032,7 +1032,7 @@ DEPENDENCIES
rack-protection (~> 2.1.0) rack-protection (~> 2.1.0)
rack-test (~> 1.1.0) rack-test (~> 1.1.0)
rack_session_access rack_session_access
rails (~> 6.0.3.2) rails (~> 6.0.3.5)
rails-controller-testing (~> 1.0.2) rails-controller-testing (~> 1.0.2)
rails-i18n (~> 6.0.0) rails-i18n (~> 6.0.0)
rdoc (>= 2.4.2) rdoc (>= 2.4.2)
@ -1081,7 +1081,7 @@ DEPENDENCIES
with_advisory_lock (~> 4.6.0) with_advisory_lock (~> 4.6.0)
RUBY VERSION RUBY VERSION
ruby 2.7.1p83 ruby 2.7.2p137
BUNDLED WITH BUNDLED WITH
2.1.4 2.1.4

@ -41,6 +41,8 @@ module VersionsHelper
def link_to_version(version, html_options = {}, options = {}) def link_to_version(version, html_options = {}, options = {})
return '' unless version&.is_a?(Version) return '' unless version&.is_a?(Version)
html_options = html_options.merge(id: link_to_version_id(version))
link_name = options[:before_text].to_s.html_safe + format_version_name(version, options[:project] || @project) link_name = options[:before_text].to_s.html_safe + format_version_name(version, options[:project] || @project)
link_to_if version.visible?, link_to_if version.visible?,
link_name, link_name,
@ -48,6 +50,10 @@ module VersionsHelper
html_options html_options
end end
def link_to_version_id(version)
ERB::Util.url_encode("version-#{version.name}")
end
def format_version_name(version, project = @project) def format_version_name(version, project = @project)
h(version.to_s_for_project(project)) h(version.to_s_for_project(project))
end end

@ -40,7 +40,7 @@ class Group < Principal
alias_attribute(:groupname, :lastname) alias_attribute(:groupname, :lastname)
validates_presence_of :groupname validates_presence_of :groupname
validate :uniqueness_of_groupname validate :uniqueness_of_groupname
validates_length_of :groupname, maximum: 30 validates_length_of :groupname, maximum: 256
# HACK: We want to have the :preference association on the Principal to allow # HACK: We want to have the :preference association on the Principal to allow
# for eager loading preferences. # for eager loading preferences.

@ -68,6 +68,6 @@ class Queries::WorkPackages::Filter::ProjectFilter < Queries::WorkPackages::Filt
private private
def visible_projects def visible_projects
@visible_projects ||= Project.visible @visible_projects ||= Project.visible.active
end end
end end

@ -129,9 +129,9 @@ class User < Principal
# Login must contain letters, numbers, underscores only # Login must contain letters, numbers, underscores only
validates_format_of :login, with: /\A[a-z0-9_\-@\.+ ]*\z/i validates_format_of :login, with: /\A[a-z0-9_\-@\.+ ]*\z/i
validates_length_of :login, maximum: 256 validates_length_of :login, maximum: 256
validates_length_of :firstname, :lastname, maximum: 30 validates_length_of :firstname, :lastname, maximum: 256
validates_format_of :mail, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, allow_blank: true validates_format_of :mail, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, allow_blank: true
validates_length_of :mail, maximum: 60, allow_nil: true validates_length_of :mail, maximum: 256, allow_nil: true
validates_confirmation_of :password, allow_nil: true validates_confirmation_of :password, allow_nil: true
validates_inclusion_of :mail_notification, in: MAIL_NOTIFICATION_OPTIONS.map(&:first), allow_blank: true validates_inclusion_of :mail_notification, in: MAIL_NOTIFICATION_OPTIONS.map(&:first), allow_blank: true

@ -118,7 +118,7 @@ class WorkPackage::Exporter::CSV < WorkPackage::Exporter::Base
when Time when Time
format_time(value) format_time(value)
when nil when nil
# ruby 2.7.1 will return a frozen string for nil.to_s which will cause an error when e.g. trying to # ruby >=2.7.1 will return a frozen string for nil.to_s which will cause an error when e.g. trying to
# force an encoding # force an encoding
'' ''
else else

@ -3,50 +3,79 @@ require_relative 'fog_file_uploader'
class DirectFogUploader < FogFileUploader class DirectFogUploader < FogFileUploader
include CarrierWaveDirect::Uploader include CarrierWaveDirect::Uploader
def self.for_attachment(attachment) ##
for_uploader attachment.file # This needs to be true so that the necessary condition is included
# in S3 upload policy (only relevant for direct uploads).
def will_include_content_type
true
end end
def self.for_uploader(fog_file_uploader) class << self
raise ArgumentError, "FogFileUploader expected" unless fog_file_uploader.is_a? FogFileUploader def for_attachment(attachment)
for_uploader attachment.file
end
uploader = self.new def for_uploader(fog_file_uploader)
raise ArgumentError, "FogFileUploader expected" unless fog_file_uploader.is_a? FogFileUploader
uploader.instance_variable_set "@file", fog_file_uploader.file uploader = self.new
uploader.instance_variable_set "@key", fog_file_uploader.path
uploader uploader.instance_variable_set "@file", fog_file_uploader.file
end uploader.instance_variable_set "@key", fog_file_uploader.path
uploader.instance_variable_set "@model", fog_file_uploader.model
## uploader
# Generates the direct upload form for the given attachment. end
#
# @param attachment [Attachment] The attachment for which a file is to be uploaded. ##
# @param success_action_redirect [String] URL to redirect to if successful (none by default, using status). # Generates the direct upload form for the given attachment.
# @param success_action_status [String] The HTTP status to return on success (201 by default). #
# @param max_file_size [Integer] The maximum file size to be allowed in bytes. # @param attachment [Attachment] The attachment for which a file is to be uploaded.
def self.direct_fog_hash( # @param success_action_redirect [String] URL to redirect to if successful (none by default, using status).
attachment:, # @param success_action_status [String] The HTTP status to return on success (201 by default).
success_action_redirect: nil, # @param max_file_size [Integer] The maximum file size to be allowed in bytes.
success_action_status: "201", def direct_fog_hash(
max_file_size: Setting.attachment_max_size * 1024 attachment:,
) success_action_redirect: nil,
uploader = for_attachment attachment success_action_status: "201",
max_file_size: Setting.attachment_max_size * 1024
if success_action_redirect.present? )
uploader.success_action_redirect = success_action_redirect uploader = direct_fog_hash_uploader attachment, success_action_redirect, success_action_status
uploader.use_action_status = false hash = uploader
else .direct_fog_hash(enforce_utf8: false, max_file_size: max_file_size)
uploader.success_action_status = success_action_status .merge(extra_fog_hash_attributes(uploader: uploader))
uploader.use_action_status = true
if success_action_redirect.present?
hash.merge(success_action_redirect: success_action_redirect)
else
hash.merge(success_action_status: success_action_status)
end
end
def extra_fog_hash_attributes(uploader:)
return {} unless include_content_type?(uploader)
{
"Content-Type": uploader.fog_attributes[:"Content-Type"]
}
end end
hash = uploader.direct_fog_hash(enforce_utf8: false, max_file_size: max_file_size) private
def include_content_type?(uploader)
uploader.will_include_content_type && uploader.fog_attributes.include?(:"Content-Type")
end
if success_action_redirect.present? def direct_fog_hash_uploader(attachment, success_action_redirect, success_action_status)
hash.merge(success_action_redirect: success_action_redirect) for_attachment(attachment).tap do |uploader|
else if success_action_redirect.present?
hash.merge(success_action_status: success_action_status) uploader.success_action_redirect = success_action_redirect
uploader.use_action_status = false
else
uploader.success_action_status = success_action_status
uploader.use_action_status = true
end
end
end end
end end
end end

@ -57,6 +57,16 @@ class FogFileUploader < CarrierWave::Uploader::Base
super super
end end
##
# This is necessary for carrierwave to set the Content-Type in the S3 metadata for instance.
def fog_attributes
content_type = model.content_type
return super if content_type.blank?
super.merge "Content-Type": content_type
end
## ##
# Generates a download URL for this file. # Generates a download URL for this file.
# #
@ -104,8 +114,7 @@ class FogFileUploader < CarrierWave::Uploader::Base
def set_expires_at!(url_options, options:) def set_expires_at!(url_options, options:)
if options[:expires_in].present? if options[:expires_in].present?
# AWS allows at max < 604800 expires time expires = [options[:expires_in], OpenProject::Configuration.fog_download_url_expires_in].min
expires = [options[:expires_in], 604799].min
url_options[:expire_at] = ::Fog::Time.now + expires url_options[:expire_at] = ::Fog::Time.now + expires
end end

@ -21,7 +21,6 @@
<% if @project.descendants.active.any? %> <% if @project.descendants.active.any? %>
<div class="form--space"></div> <div class="form--space"></div>
<%= hidden_field_tag 'with_subprojects', 0 %>
<div class="form--field -trailing-label -no-margin"> <div class="form--field -trailing-label -no-margin">
<%= styled_label_tag "with-subprojects", t(:label_subproject_plural) %> <%= styled_label_tag "with-subprojects", t(:label_subproject_plural) %>

@ -1,5 +1,8 @@
<h3><%= t(:label_version_plural) %></h3> <h3><%= t(:label_version_plural) %></h3>
<% @versions.each do |version| %> <% @versions.each do |version| %>
<%= link_to format_version_name(version), "#{project_roadmap_url}##{version.name}" %><br /> <%= link_to format_version_name(version),
project_roadmap_path({ anchor: link_to_version_id(version) }.merge(params.permit(:completed,
:with_subprojects,
type_ids: []).to_h)) %><br />
<% end %> <% end %>

@ -45,7 +45,7 @@ See docs/COPYRIGHT.rdoc for more details.
<div id="roadmap"> <div id="roadmap">
<% @versions.each do |version| %> <% @versions.each do |version| %>
<h3 class="version icon-context icon-modules"> <h3 class="version icon-context icon-modules">
<%= link_to_version version, name: h(version.name) %> <%= link_to_version(version, name: h(version.name), id: "version-#{version.name}") %>
</h3> </h3>
<%= render partial: 'versions/overview', locals: {version: version} %> <%= render partial: 'versions/overview', locals: {version: version} %>
<%= render(partial: "wiki/content", locals: {content: version.wiki_page.content}) if version.wiki_page %> <%= render(partial: "wiki/content", locals: {content: version.wiki_page.content}) if version.wiki_page %>

@ -940,8 +940,8 @@ tr:
other: "%{count} alt iş paketi" other: "%{count} alt iş paketi"
hour: hour:
zero: "0 s" zero: "0 s"
one: "%{count} 1 saat" one: "1 saat"
other: "%{count} 1 saat" other: "%{count} saat"
zen_mode: zen_mode:
button_activate: 'Zen modunu etkinleştir' button_activate: 'Zen modunu etkinleştir'
button_deactivate: 'Zen modunu devre dışı bırak' button_deactivate: 'Zen modunu devre dışı bırak'

@ -0,0 +1,13 @@
class AlterUserAttributesMaxLength < ActiveRecord::Migration[6.0]
def up
change_column :users, :firstname, :string, limit: nil
change_column :users, :lastname, :string, limit: nil
change_column :users, :mail, :string, limit: nil
end
def down
change_column :users, :firstname, :string, limit: 30
change_column :users, :lastname, :string, limit: 30
change_column :users, :mail, :string, limit: 60
end
end

@ -1,4 +1,4 @@
FROM ruby:2.7.1-buster as develop FROM ruby:2.7.2-buster as develop
MAINTAINER operations@openproject.com MAINTAINER operations@openproject.com
ARG DEV_UID=1000 ARG DEV_UID=1000

@ -1,4 +1,4 @@
FROM ruby:2.7.1-buster FROM ruby:2.7.2-buster
MAINTAINER operations@openproject.com MAINTAINER operations@openproject.com
# Allow platform-specific additions. Valid values are: on-prem,saas,bahn # Allow platform-specific additions. Valid values are: on-prem,saas,bahn

@ -49,7 +49,7 @@ The body *must* be the raw content of the file.
Note that a `filename` *must* be indicated in the `Content-Disposition` of this part, although it will be ignored. Note that a `filename` *must* be indicated in the `Content-Disposition` of this part, although it will be ignored.
Instead the `fileName` inside the JSON of the metadata part will be used. Instead the `fileName` inside the JSON of the metadata part will be used.
+ Request (multipart/form-data) + Request (multipart/form-data; boundary=boundary-delimiter)
--boundary-delimiter --boundary-delimiter
Content-Disposition: form-data; name="metadata" Content-Disposition: form-data; name="metadata"
@ -332,7 +332,7 @@ See [the general specification for uploading attachments](#attachments-attachmen
+ Parameters + Parameters
+ id (required, integer, `1`) ... ID of the post to receive the attachment + id (required, integer, `1`) ... ID of the post to receive the attachment
+ Request (multipart/form-data) + Request (multipart/form-data; boundary=boundary-delimiter)
--boundary-delimiter --boundary-delimiter
Content-Disposition: form-data; name="metadata" Content-Disposition: form-data; name="metadata"
@ -613,7 +613,7 @@ See [the general specification for uploading attachments](#attachments-attachmen
+ Parameters + Parameters
+ id (required, integer, `1`) ... ID of the wiki page to receive the attachment + id (required, integer, `1`) ... ID of the wiki page to receive the attachment
+ Request (multipart/form-data) + Request (multipart/form-data; boundary=boundary-delimiter)
--boundary-delimiter --boundary-delimiter
Content-Disposition: form-data; name="metadata" Content-Disposition: form-data; name="metadata"
@ -924,7 +924,7 @@ Instead the `fileName` inside the JSON of the metadata part will be used.
+ Parameters + Parameters
+ id (required, integer, `1`) ... ID of the work package to receive the attachment + id (required, integer, `1`) ... ID of the work package to receive the attachment
+ Request (multipart/form-data) + Request (multipart/form-data; boundary=boundary-delimiter)
--boundary-delimiter --boundary-delimiter
Content-Disposition: form-data; name="metadata" Content-Disposition: form-data; name="metadata"

@ -113,20 +113,22 @@ You can use the form and schema to be retrieve the valid attribute values and by
+ Body + Body
{ {
"project": { "_links": {
"href": "/api/v3/projects/1" "project": {
}, "href": "/api/v3/projects/1"
"principal": { },
"href": "/api/v3/users/5" "principal": {
}, "href": "/api/v3/users/5"
"roles": [ },
{ "roles": [
"href": "/api/v3/roles/5" {
}, "href": "/api/v3/roles/5"
{ },
"href": "/api/v3/roles/8" {
"href": "/api/v3/roles/8"
}
]
} }
]
} }
+ Response 201 + Response 201

@ -1,4 +1,3 @@
# Group Forms # Group Forms
This API provides forms as a concept to aid in editing or creating resources. The goal of forms is to: This API provides forms as a concept to aid in editing or creating resources. The goal of forms is to:

@ -1,7 +1,8 @@
<!-- include(introduction.apib) --> <!-- include(introduction.apib) -->
<!-- include(basic-objects.apib) --> <!-- include(basic-objects.apib) -->
<!-- include(filters.apib) -->
<!-- include(group-objects.apib) --> <!-- include(group-objects.apib) -->
<!-- include(filters.apib) -->
<!-- include(forms.apib) -->
<!-- include(endpoints/activities.apib) --> <!-- include(endpoints/activities.apib) -->
<!-- include(endpoints/attachments.apib) --> <!-- include(endpoints/attachments.apib) -->
@ -11,7 +12,6 @@
<!-- include(endpoints/custom-actions.apib) --> <!-- include(endpoints/custom-actions.apib) -->
<!-- include(endpoints/custom-options.apib) --> <!-- include(endpoints/custom-options.apib) -->
<!-- include(endpoints/documents.apib) --> <!-- include(endpoints/documents.apib) -->
<!-- include(endpoints/forms.apib) -->
<!-- include(endpoints/grids.apib) --> <!-- include(endpoints/grids.apib) -->
<!-- include(endpoints/groups.apib) --> <!-- include(endpoints/groups.apib) -->
<!-- include(endpoints/help_texts.apib) --> <!-- include(endpoints/help_texts.apib) -->

@ -32,20 +32,20 @@ $ rbenv init
**Installing ruby-2.7** **Installing ruby-2.7**
With both installed, we can now install the actual ruby version 2.7. You can check available ruby versions with `rbenv install --list`. With both installed, we can now install the actual ruby version 2.7. You can check available ruby versions with `rbenv install --list`.
At the time of this writing, the latest stable version is `2.7.1`, which we also require. At the time of this writing, the latest stable version is `2.7.2`, which we also require.
We suggest you install the version we require in the [Gemfile](https://github.com/opf/openproject/blob/dev/Gemfile). Search for the `ruby '~> X.Y.Z'` line We suggest you install the version we require in the [Gemfile](https://github.com/opf/openproject/blob/dev/Gemfile). Search for the `ruby '~> X.Y.Z'` line
and install that version. and install that version.
```bash ```bash
# Install the required version as read from the Gemfile # Install the required version as read from the Gemfile
rbenv install 2.7.1 rbenv install 2.7.2
``` ```
This might take a while depending on whether ruby is built from source. After it is complete, you need to tell rbenv to globally activate this version This might take a while depending on whether ruby is built from source. After it is complete, you need to tell rbenv to globally activate this version
```bash ```bash
rbenv global 2.7.1 rbenv global 2.7.2
``` ```
You also need to install [bundler](https://github.com/bundler/bundler/), the ruby gem bundler. You also need to install [bundler](https://github.com/bundler/bundler/), the ruby gem bundler.
@ -110,7 +110,7 @@ You should now have an active ruby and node installation. Verify that it works w
```bash ```bash
$ ruby --version $ ruby --version
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin16] ruby 2.7.2p137 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin16]
$ bundler --version $ bundler --version
Bundler version 2.0.2 Bundler version 2.0.2

@ -54,20 +54,20 @@ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
**Installing ruby-2.7** **Installing ruby-2.7**
With both installed, we can now install the actual ruby version 2.7. You can check available ruby versions with `rbenv install --list`. With both installed, we can now install the actual ruby version 2.7. You can check available ruby versions with `rbenv install --list`.
At the time of this writing, the latest stable version is `2.7.1`, which we also require. At the time of this writing, the latest stable version is `2.7.2`, which we also require.
We suggest you install the version we require in the [Gemfile](https://github.com/opf/openproject/blob/dev/Gemfile). Search for the `ruby '~> X.Y.Z'` line We suggest you install the version we require in the [Gemfile](https://github.com/opf/openproject/blob/dev/Gemfile). Search for the `ruby '~> X.Y.Z'` line
and install that version. and install that version.
```bash ```bash
# Install the required version as read from the Gemfile # Install the required version as read from the Gemfile
rbenv install 2.7.1 rbenv install 2.7.2
``` ```
This might take a while depending on whether ruby is built from source. After it is complete, you need to tell rbenv to globally activate this version This might take a while depending on whether ruby is built from source. After it is complete, you need to tell rbenv to globally activate this version
```bash ```bash
rbenv global 2.7.1 rbenv global 2.7.2
rbenv rehash rbenv rehash
``` ```
@ -149,7 +149,7 @@ You should now have an active ruby and node installation. Verify that it works w
```bash ```bash
ruby --version ruby --version
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux] ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]
bundler --version bundler --version
Bundler version 2.0.2 Bundler version 2.0.2

@ -6,19 +6,18 @@ sidebar_navigation:
# Changing to BIM Edition # Changing to BIM Edition
An existing OpenProject on-premises (self hosted) installation can easily be switched to the BIM Edition. An existing OpenProject on-premises (self hosted) installation can easily be switched to the BIM Edition. The BIM Edition extends the capabilities of a normal OpenProject installation with special features for the construction industry.
The BIM Edition extends the capabilities of a normal OpenProject installation with special features
for the construction industry.
Switching to the BIM Edition will not affect your existing data. Your team will be able to continue Switching to the BIM Edition will not affect your existing data. Your team will be able to continue working just as before. By switching to the BIM edition additional features will become available when you activate the "BCF" module in the [project's settings](../../user-guide/projects/project-settings/modules).
working just as before. By switching to the BIM edition additional features will become available
when you activate the "BCF" module a project's settings. To choose the BIM edition during installation use [this instruction](../installation/packaged/#step-1-select-your-openproject-edition).
## Instructions ## Instructions
### Backup and upgrade ### Backup and upgrade
First, backup your data and update your installation to the latest OpenProject version as described in [Upgrading](../operation/upgrading). First, backup your data and update your installation to the latest OpenProject version as described in [Upgrading](../operation/upgrading).
Make sure that you not only install the new package but also run `sudo openproject configure` as described before proceeding.
### Switching to BIM Edition ### Switching to BIM Edition

@ -39,6 +39,7 @@ Configuring OpenProject through environment variables is detailed [in this separ
* [`disable_password_login`](#disable-password-login) (default: false) * [`disable_password_login`](#disable-password-login) (default: false)
* [`attachments_storage`](#attachments-storage) (default: file) * [`attachments_storage`](#attachments-storage) (default: file)
* [`direct_uploads`](#direct-uploads) (default: true) * [`direct_uploads`](#direct-uploads) (default: true)
* [`fog_download_url_expires_in`](#fog-download-url-expires-in) (default: 21600)
* [`hidden_menu_items`](#hidden-menu-items) (default: {}) * [`hidden_menu_items`](#hidden-menu-items) (default: {})
* [`disabled_modules`](#disabled-modules) (default: []) * [`disabled_modules`](#disabled-modules) (default: [])
* [`blacklisted_routes`](#blacklisted-routes) (default: []) * [`blacklisted_routes`](#blacklisted-routes) (default: [])
@ -188,6 +189,21 @@ to the remote storage in an extra step.
**Note**: This only works for S3 right now. When using fog with another provider this configuration will be `false`. The same goes for when no fog storage is configured. **Note**: This only works for S3 right now. When using fog with another provider this configuration will be `false`. The same goes for when no fog storage is configured.
### fog download url expires in
*default: 21600*
Example:
fog_download_url_expires_in: 60
When using remote storage for attachments via fog - usually S3 (see [`attachments_storage`](#attachments-storage) option) -
each attachment download will generate a temporary URL.
This option determines how long these links will be valid.
The default is 21600 seconds, that is 6 hours, which is the maximum expiry time
allowed by S3 when using IAM roles for authentication.
### Overriding the help link ### Overriding the help link
You can override the default help menu of OpenProject by specifying a `force_help_link` option to You can override the default help menu of OpenProject by specifying a `force_help_link` option to

@ -63,7 +63,7 @@ This will execute `certbot renew` every day at 1am. The command checks if the ce
## External SSL termination ## External SSL termination
If you terminate SSL externally before the request hits the OpenProject server, you need to let the OpenProject server know that the request being handled is https, even though SSL was terminated before. This is the most common source in problems in OpenProject when using an external server that terminates SSL. If you terminate SSL externally<sup>1</sup> before the request hits the OpenProject server, you need to let the OpenProject server know that the request being handled is https, even though SSL was terminated before. This is the most common source in problems in OpenProject when using an external server that terminates SSL.
Please ensure that if you're proxying to the openproject server, you set the HOST header to the internal server. This ensures that the host name of the outer request gets forwarded to the internal server. Otherwise you might see redirects in your browser to the internal host that OpenProject is running on. Please ensure that if you're proxying to the openproject server, you set the HOST header to the internal server. This ensures that the host name of the outer request gets forwarded to the internal server. Otherwise you might see redirects in your browser to the internal host that OpenProject is running on.
@ -83,3 +83,6 @@ If you're terminating SSL on the outer server, you need to set the `X-Forwarded-
Finally, to let OpenProject know that it should create links with 'https' when no request is available (for example, when sending emails), you need to set the Protocol setting of OpenProject to `https`. You will find this setting on your system settings or via the rails console with `Setting.protocol = 'https'` Finally, to let OpenProject know that it should create links with 'https' when no request is available (for example, when sending emails), you need to set the Protocol setting of OpenProject to `https`. You will find this setting on your system settings or via the rails console with `Setting.protocol = 'https'`
_<sup>1</sup> In the packaged installation this means you selected "no" when asked for SSL in the configuration wizard but at the same time take care of SSL termination elsewhere. This can be a manual Apache setup on the same server (not recommended) or an external server, for instance._

@ -106,16 +106,16 @@ time to finish.
[openproject@host] source ~/.profile [openproject@host] source ~/.profile
[openproject@host] git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build [openproject@host] git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
[openproject@host] rbenv install 2.7.1 [openproject@host] rbenv install 2.7.2
[openproject@host] rbenv rehash [openproject@host] rbenv rehash
[openproject@host] rbenv global 2.7.1 [openproject@host] rbenv global 2.7.2
``` ```
To check our Ruby installation we run `ruby --version`. It should output To check our Ruby installation we run `ruby --version`. It should output
something very similar to: something very similar to:
``` ```
ruby 2.7.1pXYZ (....) [x86_64-linux] ruby 2.7.2pXYZ (....) [x86_64-linux]
``` ```
## Installation of Node ## Installation of Node

@ -0,0 +1,28 @@
---
title: OpenProject 11.1.3
sidebar_navigation:
title: 11.1.3
release_version: 11.1.3
release_date: 2021-02-11
---
# OpenProject 11.1.3
We released [OpenProject 11.1.3](https://community.openproject.com/versions/1469).
The release contains several bug fixes and we recommend updating to the newest version.
<!--more-->
#### Bug fixes and changes
- Fixed: Filter gets removed (ERIK@Staging) \[[#34003](https://community.openproject.com/wp/34003)\]
- Fixed: S3 presigned URL cached for 7 days does not work with IAM roles and is a security issue \[[#35739](https://community.openproject.com/wp/35739)\]
- Fixed: Images directly uploaded to s3 are not displayed within new tab \[[#36018](https://community.openproject.com/wp/36018)\]
- Fixed: Selecting "Atom" in export menu throws cryptic error \[[#36052](https://community.openproject.com/wp/36052)\]
- Fixed: Creating new synchronized groups from filters raises error if group name too long \[[#36081](https://community.openproject.com/wp/36081)\]
#### Contributions
A big thanks to community members for reporting bugs and helping us identifying and providing fixes.
Special thanks for reporting and finding bugs go to
Florian Stoyadin, Andreas Wittig

@ -12,6 +12,13 @@ Stay up to date and get an overview of the new features included in the releases
<!--- New release notes are generated below. Do not remove comment. --> <!--- New release notes are generated below. Do not remove comment. -->
<!--- RELEASE MARKER --> <!--- RELEASE MARKER -->
## 11.1.3
Release date: 2021-02-11
[Release Notes](11-1-3/)
## 11.1.2 ## 11.1.2
Release date: 2021-01-21 Release date: 2021-01-21

@ -16,6 +16,10 @@ In OpenProject EE, you can synchronize LDAP group memberships defined through th
- - have at least one group defined in OpenProject (See the “[Managing groups](../../../users-permissions/groups/)” guide for more information on how to create and edit groups), - - have at least one group defined in OpenProject (See the “[Managing groups](../../../users-permissions/groups/)” guide for more information on how to create and edit groups),
- have set up your LDAP authentication source (See the “[Manage LDAP authentication](../../ldap-authentication/)” guide) - have set up your LDAP authentication source (See the “[Manage LDAP authentication](../../ldap-authentication/)” guide)
- have at least one LDAP entry with a *groupOfNames* object class and at least one *member* reference to an entry within your base DN of your LDAP authentication source. We use the inverse *memberOf* filter to determine the members of a group entry. - have at least one LDAP entry with a *groupOfNames* object class and at least one *member* reference to an entry within your base DN of your LDAP authentication source. We use the inverse *memberOf* filter to determine the members of a group entry.
<div class="alert alert-info" role="alert">
**Please note**: OpenProject does not support other attributes other than the `member` / `memberOf` property to define groups.
</div>
For the sake of simplicity, we assume that in this guide, your LDAP structure looks like the following: For the sake of simplicity, we assume that in this guide, your LDAP structure looks like the following:

@ -4,7 +4,7 @@ sidebar_navigation:
priority: 990 priority: 990
description: Manage users in OpenProject. description: Manage users in OpenProject.
robots: index, follow robots: index, follow
keywords: manage users keywords: manage users, lock, unlock, invite, language
--- ---
# Manage Users # Manage Users
@ -16,14 +16,14 @@ The users list provides an overview of all users in OpenProject. You can create
</div> </div>
| Topic | Content | | Topic | Content |
| --------------------------------------------- | ------------------------------------------------------------ | | ----------------------------------------------- | ------------------------------------------------------------ |
| [User list](#user-list) | Manage all users in OpenProject. | | [User list](#user-list) | Manage all users in OpenProject. |
| [Lock users](#lock-users) | Block a user permanently in the system. | | [Lock and unlock users](#lock-and-unlock-users) | Block a user permanently in the system or unlock a user. |
| [Filter users](#filter-users) | Filter users in the list. | | [Filter users](#filter-users) | Filter users in the list. |
| [Invite new users](#invite-new-users) | Add new users to your OpenProject and invite them via email. Resend and delete user invitations. | | [Invite new users](#invite-new-users) | Add new users to your OpenProject and invite them via email. Resend and delete user invitations. |
| [Manage user settings](#manage-user-settings) | Manage user settings, e.g. language, projects, groups, global roles, rate history, avatar, two-factor authentication. | | [Manage user settings](#manage-user-settings) | Manage user settings, e.g. language, projects, groups, global roles, rate history, avatar, two-factor authentication. |
| [Delete users](#delete-users) | Delete a user from the system. | | [Delete users](#delete-users) | Delete a user from the system. |
## User list ## User list
@ -35,18 +35,21 @@ Also, you get the information when the user has been created, and when the user
![user list](image-20200211141841492.png) ![user list](image-20200211141841492.png)
## Lock users ## Lock and unlock users
If you want to **block users permanently** in the system, you can click the **Lock permanently** link next to a user. If you want to **block users permanently** in the system, you can click the **Lock permanently** link next to a user.
If you are using the [OpenProject Cloud Edition](../../../cloud-edition-guide), you will then have a new user available to add to the system within your booked plan. If you are using [Enterprise cloud](../../../cloud-edition-guide) or [Enterprise on-premises](../../../enterprise-edition-guide) you will then have a new user available to add to the system within your booked plan.
<div class="alert alert-info" role="alert"> <div class="alert alert-info" role="alert">
**Note**: The previous activities from this locked users will still be displayed in the system. **Note**: The previous activities from these locked users will still be displayed in the system.
</div> </div>
![System-admin-guide_lock-users](System-admin-guide_lock-users.png) ![System-admin-guide_lock-users](System-admin-guide_lock-users.png)
The way to unlock users is basically the same. Use the **Unlock** link at the right.
Here you can also **unlock users who have been locked temporarily due to multiple failed login attempts**.
## Filter users ## Filter users
Especiall if you have a very long user list, it is essential to filter in this list. Especiall if you have a very long user list, it is essential to filter in this list.

@ -35,18 +35,23 @@ You can create a comparison of two versions to see the changes made for specific
## Working with an SVN client ## Working with an SVN or Git client
The data contained in a project repository can be downloaded to your computer using one of several clients, for example [Tortoise SVN](https://tortoisesvn.net/). The data contained in a project repository can be downloaded to your computer using one of several clients, for example [Tortoise SVN](https://tortoisesvn.net/) for Subversion, and the [git client](https://git-scm.com/) or [one of the recommended GUI clients](https://git-scm.com/downloads/guis) for Git.
The specifics of working of the selected version control client may vary. Please refer to the documentation of your version control software client for more information.
If you choose to use Tortoise SVN, you will find a good guide [here](http://tortoisesvn.net/docs/release/TortoiseSVN_en/tsvn-dug.html).
For Git, we recommend the [Pro Git guide](https://git-scm.com/book/en/v2).
The specifics of working of the selected version control client may vary. Please refer to the documentation of your version control software client for more information. If you choose to use Tortoise SVN, you will find a good guide [here](http://tortoisesvn.net/docs/release/TortoiseSVN_en/tsvn-dug.html). The specifics of working of the selected version control client may vary. Please refer to the documentation of your version control software client for more information. If you choose to use Tortoise SVN, you will find a good guide [here](http://tortoisesvn.net/docs/release/TortoiseSVN_en/tsvn-dug.html).
## Referencing work packages
In the commit message you can reference a workpackge ID (e.g. #1234). In the repository settings (Administration -> System settings -> Repository) you can define keywords that change the status of the referenced work package (e.g. fixes #1234 or closes #1234). In the commit message you can reference a workpackge ID (e.g. #1234). In the repository settings (Administration -> System settings -> Repository) you can define keywords that change the status of the referenced work package (e.g. fixes #1234 or closes #1234).
In any textile field you can reference revisions by putting an "r" in front of the revision number (e.g. r123). In any textile field you can reference revisions by putting an "r" in front of the revision number (e.g. r123).
## Configure Repositories in OpenProject ## Configure Repositories in OpenProject
Please see our system admin guide [how to configure repositories in OpenProject](../../system-admin-guide/system-settings/repositories/). Please see our system admin guide [how to configure repositories in OpenProject](../../system-admin-guide/system-settings/repositories/).
@ -55,4 +60,4 @@ Please see our system admin guide [how to configure repositories in OpenProject]
## Repository integration ## Repository integration
See our Installation and operations guide how to [integrate repositories in Openproject](../../installation-and-operations/configuration/repositories/#repository-integration-in-openproject). See our Installation and operations guide how to [integrate repositories in Openproject](../../installation-and-operations/configuration/repositories/#repository-integration-in-openproject).

@ -73,6 +73,10 @@ The results will be displayed accordingly in the work package list.
![filter-text](filter-text.png) ![filter-text](filter-text.png)
<div class="alert alert-info" role="alert">
**Good to know**: Filtering a work packages list will temporarily change the default work package type and default status according to your filters to make newly created work packages visible in the list.
</div>
## Sort the work package list ## Sort the work package list
### Automatic sorting of the work package list ### Automatic sorting of the work package list

@ -8,7 +8,7 @@ import {HalLink} from "core-app/modules/hal/hal-link/hal-link";
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {OpModalLocalsToken} from "core-components/op-modals/op-modal.service"; import {OpModalLocalsToken} from "core-components/op-modals/op-modal.service";
import * as URI from 'urijs'; import * as URI from 'urijs';
import {HttpClient} from '@angular/common/http'; import {HttpClient, HttpErrorResponse} from '@angular/common/http';
import {LoadingIndicatorService} from "core-app/modules/common/loading-indicator/loading-indicator.service"; import {LoadingIndicatorService} from "core-app/modules/common/loading-indicator/loading-indicator.service";
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';
import {NotificationsService} from "core-app/modules/common/notifications/notifications.service"; import {NotificationsService} from "core-app/modules/common/notifications/notifications.service";
@ -105,8 +105,21 @@ export class WpTableExportModal extends OpModalComponent implements OnInit {
this.service.show(JobStatusModal, 'global', { jobId: jobId }); this.service.show(JobStatusModal, 'global', { jobId: jobId });
} }
private handleError(error:string) { private handleError(error:HttpErrorResponse) {
this.notifications.addError(error || this.I18n.t('js.error.internal')); // There was an error but the status code is actually a 200.
// If that is the case the response's content-type probably does not match
// the expected type (json).
// Currently this happens e.g. when exporting Atom which actually is not an export
// but rather a feed to follow.
if (error.status === 200 && error.url) {
window.open(error.url);
} else {
this.showError(error);
}
}
private showError(error:HttpErrorResponse) {
this.notifications.addError(error.message || this.I18n.t('js.error.internal'));
} }
private addColumnsToHref(href:string) { private addColumnsToHref(href:string) {

@ -1,31 +1,29 @@
<div class="textarea-wrapper"> <ng-select [(ngModel)]="selectedOption"
<ng-select [(ngModel)]="selectedOption" [ngClass]="'inline-edit--field -multi-select'"
[ngClass]="'inline-edit--field -multi-select'" [required]="required"
[required]="required" [clearable]="!required"
[clearable]="!required" [disabled]="inFlight"
[disabled]="inFlight" [id]="handler.htmlId"
[id]="handler.htmlId" [items]="valueOptions"
[items]="valueOptions" bindLabel="name"
bindLabel="name" [virtualScroll]="true"
[virtualScroll]="true" [clearSearchOnAdd]="true"
[clearSearchOnAdd]="true" (keydown)="handler.handleUserKeydown($event, true)"
(keydown)="handler.handleUserKeydown($event, true)" (open)="onOpen()"
(open)="onOpen()" (close)="onClose()"
(close)="onClose()" (add)="repositionDropdown()"
(add)="repositionDropdown()" (remove)="repositionDropdown()"
(remove)="repositionDropdown()" [multiple]="true"
[multiple]="true" [closeOnSelect]="false"
[closeOnSelect]="false" [appendTo]="appendTo"
[appendTo]="appendTo" [dropdownPosition]="'top'"
[dropdownPosition]="'top'" [hideSelected]="true">
[hideSelected]="true"> </ng-select>
</ng-select>
<edit-field-controls [fieldController]="self" <edit-field-controls [fieldController]="self"
*ngIf="!handler.inEditMode" *ngIf="!handler.inEditMode"
(onSave)="handler.handleUserSubmit()" (onSave)="handler.handleUserSubmit()"
(onCancel)="handler.handleUserCancel()" (onCancel)="handler.handleUserCancel()"
[saveTitle]="text.save" [saveTitle]="text.save"
[cancelTitle]="text.cancel"> [cancelTitle]="text.cancel">
</edit-field-controls> </edit-field-controls>
</div>

@ -25,6 +25,7 @@
{{ text.download_starts }} {{ text.download_starts }}
<a #downloadLink <a #downloadLink
download download
target="_blank"
[textContent]="text.click_to_download" [textContent]="text.click_to_download"
[attr.href]="downloadHref"> [attr.href]="downloadHref">
</a> </a>

@ -35,7 +35,10 @@ module API
FilterDependencyRepresenter FilterDependencyRepresenter
def href_callback def href_callback
api_v3_paths.projects params = [active: { operator: '=', values: ['t'] }]
escaped = CGI.escape(::JSON.dump(params))
"#{api_v3_paths.projects}?filters=#{escaped}"
end end
def type def type

@ -50,6 +50,7 @@ module OpenProject
# which will be uploaded directly to the cloud storage rather than via OpenProject's # which will be uploaded directly to the cloud storage rather than via OpenProject's
# server process. # server process.
'direct_uploads' => true, 'direct_uploads' => true,
'fog_download_url_expires_in' => 21600, # 6h by default as 6 hours is max in S3 when using IAM roles
'show_community_links' => true, 'show_community_links' => true,
'log_level' => 'info', 'log_level' => 'info',
'scm_git_command' => nil, 'scm_git_command' => nil,

@ -34,7 +34,7 @@ module OpenProject
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 11 MAJOR = 11
MINOR = 1 MINOR = 1
PATCH = 2 PATCH = 3
TINY = PATCH # Redmine compat TINY = PATCH # Redmine compat
class << self class << self

@ -43,8 +43,8 @@ uk:
invalid_statuses_found: 'Invalid status names found' invalid_statuses_found: 'Invalid status names found'
invalid_priorities_found: 'Invalid priority names found' invalid_priorities_found: 'Invalid priority names found'
invalid_emails_found: 'Invalid email addresses found' invalid_emails_found: 'Invalid email addresses found'
unknown_emails_found: 'Unknown email addresses found' unknown_emails_found: 'Знайдено невідомі адреси електронної пошти'
unknown_property: 'Unknown property' unknown_property: 'Невідома властивість'
non_members_found: 'Non project members found' non_members_found: 'Non project members found'
import_types_as: 'Set all these types to' import_types_as: 'Set all these types to'
import_statuses_as: 'Set all these statuses to' import_statuses_as: 'Set all these statuses to'

@ -68,8 +68,8 @@ describe 'API v3 Attachments by budget resource', type: :request do
let(:permissions) { %i[view_budgets edit_budgets] } let(:permissions) { %i[view_budgets edit_budgets] }
let(:request_path) { api_v3_paths.attachments_by_budget budget.id } let(:request_path) { api_v3_paths.attachments_by_budget budget.id }
let(:request_parts) { { metadata: metadata, file: file } } let(:request_parts) { { metadata: metadata.to_json, file: file } }
let(:metadata) { { fileName: 'cat.png' }.to_json } let(:metadata) { { fileName: 'cat.png' } }
let(:file) { mock_uploaded_file(name: 'original-filename.txt') } let(:file) { mock_uploaded_file(name: 'original-filename.txt') }
let(:max_file_size) { 1 } # given in kiB let(:max_file_size) { 1 } # given in kiB
@ -99,19 +99,19 @@ describe 'API v3 Attachments by budget resource', type: :request do
context 'file section is missing' do context 'file section is missing' do
# rack-test won't send a multipart request without a file being present # 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 # however as long as we depend on correctly named sections this test should do just fine
let(:request_parts) { { metadata: metadata, wrongFileSection: file } } 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 'invalid request body', I18n.t('api_v3.errors.multipart_body_error')
end end
context 'metadata section is no valid JSON' do context 'metadata section is no valid JSON' do
let(:metadata) { '"fileName": "cat.png"' } let(:request_parts) { { metadata: '"fileName": "cat.png"', file: file } }
it_behaves_like 'parse error' it_behaves_like 'parse error'
end end
context 'metadata is missing the fileName' do context 'metadata is missing the fileName' do
let(:metadata) { Hash.new.to_json } let(:metadata) { Hash.new }
it_behaves_like 'constraint violation' do it_behaves_like 'constraint violation' do
let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" } let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" }

@ -69,8 +69,8 @@ describe 'API v3 Attachments by document resource', type: :request do
let(:permissions) { %i[view_documents manage_documents] } let(:permissions) { %i[view_documents manage_documents] }
let(:request_path) { api_v3_paths.attachments_by_document document.id } let(:request_path) { api_v3_paths.attachments_by_document document.id }
let(:request_parts) { { metadata: metadata, file: file } } let(:request_parts) { { metadata: metadata.to_json, file: file } }
let(:metadata) { { fileName: 'cat.png' }.to_json } let(:metadata) { { fileName: 'cat.png' } }
let(:file) { mock_uploaded_file(name: 'original-filename.txt') } let(:file) { mock_uploaded_file(name: 'original-filename.txt') }
let(:max_file_size) { 1 } # given in kiB let(:max_file_size) { 1 } # given in kiB
@ -100,19 +100,19 @@ describe 'API v3 Attachments by document resource', type: :request do
context 'file section is missing' do context 'file section is missing' do
# rack-test won't send a multipart request without a file being present # 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 # however as long as we depend on correctly named sections this test should do just fine
let(:request_parts) { { metadata: metadata, wrongFileSection: file } } 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 'invalid request body', I18n.t('api_v3.errors.multipart_body_error')
end end
context 'metadata section is no valid JSON' do context 'metadata section is no valid JSON' do
let(:metadata) { '"fileName": "cat.png"' } let(:request_parts) { { metadata: '"fileName": "cat.png"', file: file } }
it_behaves_like 'parse error' it_behaves_like 'parse error'
end end
context 'metadata is missing the fileName' do context 'metadata is missing the fileName' do
let(:metadata) { Hash.new.to_json } let(:metadata) { Hash.new }
it_behaves_like 'constraint violation' do it_behaves_like 'constraint violation' do
let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" } let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" }

@ -18,6 +18,7 @@ module LdapGroups
validates_presence_of :dn validates_presence_of :dn
validates_presence_of :group validates_presence_of :group
validates_associated :group
validates_presence_of :auth_source validates_presence_of :auth_source
before_destroy :remove_all_members before_destroy :remove_all_members

@ -77,7 +77,7 @@ module OpenProject::LdapGroups
Group.where(id: sync.group_id).update_all(lastname: name) Group.where(id: sync.group_id).update_all(lastname: name)
else else
# Create an OpenProject group # Create an OpenProject group
sync.group = Group.find_or_initialize_by(groupname: name) sync.group = Group.find_or_create_by!(groupname: name)
end end
end end

@ -242,4 +242,31 @@ describe 'work package export', type: :feature do
end end
end end
end end
# Atom exports are not downloaded. In fact, it is not even a download but rather
# a feed one can follow.
context 'Atom export', js: true do
let(:export_type) { 'Atom' }
context 'with default filter' do
before do
work_packages_page.visit_index
filters.expect_filter_count 1
filters.open
end
it 'shows an xml with work packages' do
settings_menu.open_and_choose 'Export ...'
# The feed is opened in a new tab
new_window = window_opened_by { click_on export_type }
within_window new_window do
expect(page).to have_text(wp_1.description)
expect(page).to have_text(wp_2.description)
expect(page).to have_text(wp_3.description)
expect(page).to have_text(wp_4.description)
end
end
end
end
end end

@ -59,7 +59,8 @@ describe VersionsHelper, type: :helper do
context 'a version' do context 'a version' do
context 'with being allowed to see the version' do context 'with being allowed to see the version' do
it 'does not create a link, without permission' do it 'does not create a link, without permission' do
expect(link_to_version(version)).to eq("#{test_project.name} - #{version.name}") expect(link_to_version(version))
.to eq("#{test_project.name} - #{version.name}")
end end
end end
@ -71,21 +72,22 @@ describe VersionsHelper, type: :helper do
end end
it 'generates a link' do it 'generates a link' do
expect(link_to_version(version)).to eq("<a href=\"/versions/#{version.id}\">#{test_project.name} - #{version.name}</a>") expect(link_to_version(version))
.to be_html_eql("<a href=\"/versions/#{version.id}\" id=\"version-#{ERB::Util.url_encode(version.name)}\">#{test_project.name} - #{version.name}</a>")
end end
it 'generates a link within a project' do it 'generates a link within a project' do
@project = test_project @project = test_project
expect(link_to_version(version)).to eq("<a href=\"/versions/#{version.id}\">#{version.name}</a>") expect(link_to_version(version))
.to be_html_eql("<a href=\"/versions/#{version.id}\" id=\"version-#{ERB::Util.url_encode(version.name)}\">#{version.name}</a>")
end end
end end
end end
describe 'an invalid version' do describe '#link_to_version_id' do
let(:version) { Object } it 'generates an escaped id' do
expect(link_to_version_id(version))
it 'does not generate a link' do .to eql("version-#{ERB::Util.url_encode(version.name)}")
expect(link_to_version(Object)).to be_empty
end end
end end
end end

@ -47,7 +47,8 @@ describe ::API::V3::Queries::Schemas::ProjectFilterDependencyRepresenter, clear_
describe 'values' do describe 'values' do
let(:path) { 'values' } let(:path) { 'values' }
let(:type) { '[]Project' } let(:type) { '[]Project' }
let(:href) { api_v3_paths.projects } let(:filters) { "?filters=%5B%7B%22active%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%22t%22%5D%7D%7D%5D" }
let(:href) { api_v3_paths.projects + filters}
context "for operator 'Queries::Operators::Equals'" do context "for operator 'Queries::Operators::Equals'" do
let(:operator) { Queries::Operators::Equals } let(:operator) { Queries::Operators::Equals }

@ -318,7 +318,7 @@ describe Attachment, type: :model do
let(:url_options) { { expires_in: 1.year } } let(:url_options) { { expires_in: 1.year } }
it "uses the allowed max" do it "uses the allowed max" do
expect(query).to include "X-Amz-Expires=604799" expect(query).to include "X-Amz-Expires=#{OpenProject::Configuration.fog_download_url_expires_in}"
end end
end end
end end

@ -49,6 +49,29 @@ describe Group, type: :model do
expect(g.save).to eq true expect(g.save).to eq true
end end
describe 'with long but allowed attributes' do
it 'is valid' do
group.groupname = 'a' * 256
expect(group).to be_valid
expect(group.save).to be_truthy
end
end
describe 'with a name too long' do
it 'is invalid' do
group.groupname = 'a' * 257
expect(group).not_to be_valid
expect(group.save).to be_falsey
end
end
describe 'a user with and overly long firstname (> 256 chars)' do
it 'is invalid' do
user.firstname = 'a' * 257
expect(user).not_to be_valid
expect(user.save).to be_falsey
end
end
describe 'from legacy specs' do describe 'from legacy specs' do
let!(:roles) { FactoryBot.create_list :role, 2 } let!(:roles) { FactoryBot.create_list :role, 2 }

@ -0,0 +1,53 @@
#-- 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 Queries::WorkPackages::Filter::ProjectFilter, type: :model do
let(:query) { FactoryBot.build :query }
let(:instance) do
described_class.create!(name: 'project', context: query, operator: '=', values: [])
end
describe '#allowed_values' do
let!(:project) { FactoryBot.create :project }
let!(:archived_project) { FactoryBot.create :project, active: false }
let(:user) { FactoryBot.create(:user, member_in_projects: [project, archived_project], member_through_role: role) }
let(:role) { FactoryBot.create :role, permissions: %i(view_work_packages) }
before do
login_as user
end
it 'does not include the archived project (Regression #36026)' do
expect(instance.allowed_values)
.to match_array [[project.name, project.id.to_s]]
end
end
end

@ -45,7 +45,7 @@ describe Queries::WorkPackages::Filter::ProjectFilter, type: :model do
it 'is true if the user can see project' do it 'is true if the user can see project' do
allow(Project) allow(Project)
.to receive_message_chain(:visible, :exists?) .to receive_message_chain(:visible, :active, :exists?)
.and_return(true) .and_return(true)
expect(instance).to be_available expect(instance).to be_available
@ -53,7 +53,7 @@ describe Queries::WorkPackages::Filter::ProjectFilter, type: :model do
it 'is true if the user can not see project' do it 'is true if the user can not see project' do
allow(Project) allow(Project)
.to receive_message_chain(:visible, :exists?) .to receive_message_chain(:visible, :active, :exists?)
.and_return(false) .and_return(false)
expect(instance).to_not be_available expect(instance).to_not be_available
@ -71,7 +71,7 @@ describe Queries::WorkPackages::Filter::ProjectFilter, type: :model do
visible_projects = [parent, child] visible_projects = [parent, child]
allow(Project) allow(Project)
.to receive(:visible) .to receive_message_chain(:visible, :active)
.and_return(visible_projects) .and_return(visible_projects)
allow(Project) allow(Project)
@ -99,7 +99,7 @@ describe Queries::WorkPackages::Filter::ProjectFilter, type: :model do
before do before do
allow(Project) allow(Project)
.to receive(:visible) .to receive_message_chain(:visible, :active)
.and_return([project, project2]) .and_return([project, project2])
instance.values = [project.id.to_s] instance.values = [project.id.to_s]

@ -98,6 +98,34 @@ describe User, type: :model do
end end
end end
describe 'with long but allowed attributes' do
it 'is valid' do
user.firstname = 'a' * 256
user.lastname = 'b' * 256
user.mail = 'fo' + ('o' * 237) + '@mail.example.com'
expect(user).to be_valid
expect(user.save).to be_truthy
end
end
describe 'a user with and overly long firstname (> 256 chars)' do
it 'is invalid' do
user.firstname = 'a' * 257
expect(user).not_to be_valid
expect(user.save).to be_falsey
end
end
describe 'a user with and overly long lastname (> 256 chars)' do
it 'is invalid' do
user.lastname = 'a' * 257
expect(user).not_to be_valid
expect(user.save).to be_falsey
end
end
describe 'login whitespace' do describe 'login whitespace' do
before do before do
user.login = login user.login = login

@ -42,8 +42,8 @@ shared_examples 'it supports direct uploads' do
end end
describe 'POST /prepare', with_settings: { attachment_max_size: 512 } do describe 'POST /prepare', with_settings: { attachment_max_size: 512 } do
let(:request_parts) { { metadata: metadata, file: file } } let(:request_parts) { { metadata: metadata.to_json, file: file } }
let(:metadata) { { fileName: 'cat.png', fileSize: file.size }.to_json } let(:metadata) { { fileName: 'cat.png', fileSize: file.size, contentType: 'image/png' } }
let(:file) { mock_uploaded_file(name: 'original-filename.txt') } let(:file) { mock_uploaded_file(name: 'original-filename.txt') }
def request! def request!
@ -68,7 +68,7 @@ shared_examples 'it supports direct uploads' do
end end
context 'with no filesize metadata' do context 'with no filesize metadata' do
let(:metadata) { { fileName: 'cat.png' }.to_json } let(:metadata) { { fileName: 'cat.png' } }
it 'should respond with 422 due to missing file size metadata' do it 'should respond with 422 due to missing file size metadata' do
expect(subject.status).to eq(422) expect(subject.status).to eq(422)
@ -125,8 +125,21 @@ shared_examples 'it supports direct uploads' do
"success_action_status" "success_action_status"
) )
expect(fields["Content-Type"]).to eq metadata[:contentType]
expect(fields["key"]).to end_with "cat.png" expect(fields["key"]).to end_with "cat.png"
end end
it 'should also include the content type and the necessary policy in the form fields' do
fields = link["form_fields"]
expect(fields).to include("policy", "Content-Type")
expect(fields["Content-Type"]).to eq metadata[:contentType]
policy = Base64.decode64 fields["policy"]
expect(policy).to include '["starts-with","$Content-Type",""]'
end
end end
end end
end end
@ -214,8 +227,8 @@ shared_examples 'an APIv3 attachment resource', type: :request, content_type: :j
let(:permissions) { Array(update_permission) } let(:permissions) { Array(update_permission) }
let(:request_path) { api_v3_paths.attachments } let(:request_path) { api_v3_paths.attachments }
let(:request_parts) { { metadata: metadata, file: file } } let(:request_parts) { { metadata: metadata.to_json, file: file } }
let(:metadata) { { fileName: 'cat.png' }.to_json } let(:metadata) { { fileName: 'cat.png' } }
let(:file) { mock_uploaded_file(name: 'original-filename.txt') } let(:file) { mock_uploaded_file(name: 'original-filename.txt') }
let(:max_file_size) { 1 } # given in kiB let(:max_file_size) { 1 } # given in kiB
@ -248,19 +261,19 @@ shared_examples 'an APIv3 attachment resource', type: :request, content_type: :j
context 'file section is missing' do context 'file section is missing' do
# rack-test won't send a multipart request without a file being present # 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 # however as long as we depend on correctly named sections this test should do just fine
let(:request_parts) { { metadata: metadata, wrongFileSection: file } } 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 'invalid request body', I18n.t('api_v3.errors.multipart_body_error')
end end
context 'metadata section is no valid JSON' do context 'metadata section is no valid JSON' do
let(:metadata) { '"fileName": "cat.png"' } let(:request_parts) { { metadata: '"fileName": "cat.png"', file: file } }
it_behaves_like 'parse error' it_behaves_like 'parse error'
end end
context 'metadata is missing the fileName' do context 'metadata is missing the fileName' do
let(:metadata) { Hash.new.to_json } let(:metadata) { Hash.new }
it_behaves_like 'constraint violation' do it_behaves_like 'constraint violation' do
let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" } let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" }
@ -496,8 +509,8 @@ shared_examples 'an APIv3 attachment resource', type: :request, content_type: :j
describe '#post' do describe '#post' do
let(:request_path) { api_v3_paths.send "attachments_by_#{attachment_type}", container.id } let(:request_path) { api_v3_paths.send "attachments_by_#{attachment_type}", container.id }
let(:request_parts) { { metadata: metadata, file: file } } let(:request_parts) { { metadata: metadata.to_json, file: file } }
let(:metadata) { { fileName: 'cat.png' }.to_json } let(:metadata) { { fileName: 'cat.png' } }
let(:file) { mock_uploaded_file(name: 'original-filename.txt') } let(:file) { mock_uploaded_file(name: 'original-filename.txt') }
let(:max_file_size) { 1 } # given in kiB let(:max_file_size) { 1 } # given in kiB
@ -527,19 +540,19 @@ shared_examples 'an APIv3 attachment resource', type: :request, content_type: :j
context 'file section is missing' do context 'file section is missing' do
# rack-test won't send a multipart request without a file being present # 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 # however as long as we depend on correctly named sections this test should do just fine
let(:request_parts) { { metadata: metadata, wrongFileSection: file } } 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 'invalid request body', I18n.t('api_v3.errors.multipart_body_error')
end end
context 'metadata section is no valid JSON' do context 'metadata section is no valid JSON' do
let(:metadata) { '"fileName": "cat.png"' } let(:request_parts) { { metadata: '"fileName": "cat.png"', file: file } }
it_behaves_like 'parse error' it_behaves_like 'parse error'
end end
context 'metadata is missing the fileName' do context 'metadata is missing the fileName' do
let(:metadata) { Hash.new.to_json } let(:metadata) { Hash.new }
it_behaves_like 'constraint violation' do it_behaves_like 'constraint violation' do
let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" } let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" }

@ -50,8 +50,8 @@ describe API::V3::Attachments::AttachmentsAPI, type: :request do
let(:permissions) { [] } let(:permissions) { [] }
let(:request_path) { api_v3_paths.prepare_new_attachment_upload } let(:request_path) { api_v3_paths.prepare_new_attachment_upload }
let(:request_parts) { { metadata: metadata, file: file } } let(:request_parts) { { metadata: metadata.to_json, file: file } }
let(:metadata) { { fileName: 'cat.png' }.to_json } let(:metadata) { { fileName: 'cat.png' } }
let(:file) { mock_uploaded_file(name: 'original-filename.txt') } let(:file) { mock_uploaded_file(name: 'original-filename.txt') }
before do before do

@ -386,11 +386,8 @@ describe MailHandler, type: :model do
['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'], ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'], ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'], ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
# TODO: implement https://github.com/redmine/redmine/commit/a00f04886fac78e489bb030d20414ebdf10841e3 ['jsmith@example.net', 'AVeryLongFirstnameThatNoLongerExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatNoLongerExceedsTheMaximumLength', 'Smith'],
# ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'], ['jsmith@example.net', 'John AVeryLongLastnameThatNoLongerExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatNoLongerExceedsTheMaximumLength']
# ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', '-', 'Smith'],
['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', '-']
} }
to_test.each do |attrs, expected| to_test.each do |attrs, expected|

Loading…
Cancel
Save