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. 51
      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. 2
      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. 15
      docs/system-admin-guide/users-permissions/users/README.md
  34. 11
      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. 10
      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
if: github.repository == 'opf/openproject'
runs-on: ubuntu-latest
env:
INPUT_BUILDOPTIONS: --pull
steps:
- uses: actions/checkout@master
- name: Prepare docker files

@ -1 +1 @@
2.7.1
2.7.2

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

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

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

@ -41,6 +41,8 @@ module VersionsHelper
def link_to_version(version, html_options = {}, options = {})
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_to_if version.visible?,
link_name,
@ -48,6 +50,10 @@ module VersionsHelper
html_options
end
def link_to_version_id(version)
ERB::Util.url_encode("version-#{version.name}")
end
def format_version_name(version, project = @project)
h(version.to_s_for_project(project))
end

@ -40,7 +40,7 @@ class Group < Principal
alias_attribute(:groupname, :lastname)
validates_presence_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
# for eager loading preferences.

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

@ -129,9 +129,9 @@ class User < Principal
# Login must contain letters, numbers, underscores only
validates_format_of :login, with: /\A[a-z0-9_\-@\.+ ]*\z/i
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_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_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
format_time(value)
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
''
else

@ -3,17 +3,26 @@ require_relative 'fog_file_uploader'
class DirectFogUploader < FogFileUploader
include CarrierWaveDirect::Uploader
def self.for_attachment(attachment)
##
# 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
class << self
def for_attachment(attachment)
for_uploader attachment.file
end
def self.for_uploader(fog_file_uploader)
def for_uploader(fog_file_uploader)
raise ArgumentError, "FogFileUploader expected" unless fog_file_uploader.is_a? FogFileUploader
uploader = self.new
uploader.instance_variable_set "@file", fog_file_uploader.file
uploader.instance_variable_set "@key", fog_file_uploader.path
uploader.instance_variable_set "@model", fog_file_uploader.model
uploader
end
@ -25,28 +34,48 @@ class DirectFogUploader < FogFileUploader
# @param success_action_redirect [String] URL to redirect to if successful (none by default, using status).
# @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.
def self.direct_fog_hash(
def direct_fog_hash(
attachment:,
success_action_redirect: nil,
success_action_status: "201",
max_file_size: Setting.attachment_max_size * 1024
)
uploader = for_attachment attachment
uploader = direct_fog_hash_uploader attachment, success_action_redirect, success_action_status
hash = uploader
.direct_fog_hash(enforce_utf8: false, max_file_size: max_file_size)
.merge(extra_fog_hash_attributes(uploader: uploader))
if success_action_redirect.present?
uploader.success_action_redirect = success_action_redirect
uploader.use_action_status = false
hash.merge(success_action_redirect: success_action_redirect)
else
uploader.success_action_status = success_action_status
uploader.use_action_status = true
hash.merge(success_action_status: success_action_status)
end
end
def extra_fog_hash_attributes(uploader:)
return {} unless include_content_type?(uploader)
hash = uploader.direct_fog_hash(enforce_utf8: false, max_file_size: max_file_size)
{
"Content-Type": uploader.fog_attributes[:"Content-Type"]
}
end
private
def include_content_type?(uploader)
uploader.will_include_content_type && uploader.fog_attributes.include?(:"Content-Type")
end
def direct_fog_hash_uploader(attachment, success_action_redirect, success_action_status)
for_attachment(attachment).tap do |uploader|
if success_action_redirect.present?
hash.merge(success_action_redirect: success_action_redirect)
uploader.success_action_redirect = success_action_redirect
uploader.use_action_status = false
else
hash.merge(success_action_status: success_action_status)
uploader.success_action_status = success_action_status
uploader.use_action_status = true
end
end
end
end
end

@ -57,6 +57,16 @@ class FogFileUploader < CarrierWave::Uploader::Base
super
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.
#
@ -104,8 +114,7 @@ class FogFileUploader < CarrierWave::Uploader::Base
def set_expires_at!(url_options, options:)
if options[:expires_in].present?
# AWS allows at max < 604800 expires time
expires = [options[:expires_in], 604799].min
expires = [options[:expires_in], OpenProject::Configuration.fog_download_url_expires_in].min
url_options[:expire_at] = ::Fog::Time.now + expires
end

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

@ -1,5 +1,8 @@
<h3><%= t(:label_version_plural) %></h3>
<% @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 %>

@ -45,7 +45,7 @@ See docs/COPYRIGHT.rdoc for more details.
<div id="roadmap">
<% @versions.each do |version| %>
<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>
<%= render partial: 'versions/overview', locals: {version: version} %>
<%= render(partial: "wiki/content", locals: {content: version.wiki_page.content}) if version.wiki_page %>

@ -940,8 +940,8 @@ tr:
other: "%{count} alt iş paketi"
hour:
zero: "0 s"
one: "%{count} 1 saat"
other: "%{count} 1 saat"
one: "1 saat"
other: "%{count} saat"
zen_mode:
button_activate: 'Zen modunu etkinleştir'
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
ARG DEV_UID=1000

@ -1,4 +1,4 @@
FROM ruby:2.7.1-buster
FROM ruby:2.7.2-buster
MAINTAINER operations@openproject.com
# 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.
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
Content-Disposition: form-data; name="metadata"
@ -332,7 +332,7 @@ See [the general specification for uploading attachments](#attachments-attachmen
+ Parameters
+ 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
Content-Disposition: form-data; name="metadata"
@ -613,7 +613,7 @@ See [the general specification for uploading attachments](#attachments-attachmen
+ Parameters
+ 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
Content-Disposition: form-data; name="metadata"
@ -924,7 +924,7 @@ Instead the `fileName` inside the JSON of the metadata part will be used.
+ Parameters
+ 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
Content-Disposition: form-data; name="metadata"

@ -113,6 +113,7 @@ You can use the form and schema to be retrieve the valid attribute values and by
+ Body
{
"_links": {
"project": {
"href": "/api/v3/projects/1"
},
@ -128,6 +129,7 @@ You can use the form and schema to be retrieve the valid attribute values and by
}
]
}
}
+ Response 201

@ -1,4 +1,3 @@
# Group Forms
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(basic-objects.apib) -->
<!-- include(filters.apib) -->
<!-- include(group-objects.apib) -->
<!-- include(filters.apib) -->
<!-- include(forms.apib) -->
<!-- include(endpoints/activities.apib) -->
<!-- include(endpoints/attachments.apib) -->
@ -11,7 +12,6 @@
<!-- include(endpoints/custom-actions.apib) -->
<!-- include(endpoints/custom-options.apib) -->
<!-- include(endpoints/documents.apib) -->
<!-- include(endpoints/forms.apib) -->
<!-- include(endpoints/grids.apib) -->
<!-- include(endpoints/groups.apib) -->
<!-- include(endpoints/help_texts.apib) -->

@ -32,20 +32,20 @@ $ rbenv init
**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`.
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
and install that version.
```bash
# 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
```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.
@ -110,7 +110,7 @@ You should now have an active ruby and node installation. Verify that it works w
```bash
$ 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 2.0.2

@ -54,20 +54,20 @@ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
**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`.
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
and install that version.
```bash
# 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
```bash
rbenv global 2.7.1
rbenv global 2.7.2
rbenv rehash
```
@ -149,7 +149,7 @@ You should now have an active ruby and node installation. Verify that it works w
```bash
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 2.0.2

@ -6,19 +6,18 @@ sidebar_navigation:
# Changing to 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.
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.
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 a project's settings.
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).
To choose the BIM edition during installation use [this instruction](../installation/packaged/#step-1-select-your-openproject-edition).
## Instructions
### Backup and upgrade
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

@ -39,6 +39,7 @@ Configuring OpenProject through environment variables is detailed [in this separ
* [`disable_password_login`](#disable-password-login) (default: false)
* [`attachments_storage`](#attachments-storage) (default: file)
* [`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: {})
* [`disabled_modules`](#disabled-modules) (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.
### 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
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
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.
@ -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'`
_<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] 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 global 2.7.1
[openproject@host] rbenv global 2.7.2
```
To check our Ruby installation we run `ruby --version`. It should output
something very similar to:
```
ruby 2.7.1pXYZ (....) [x86_64-linux]
ruby 2.7.2pXYZ (....) [x86_64-linux]
```
## 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. -->
<!--- RELEASE MARKER -->
## 11.1.3
Release date: 2021-02-11
[Release Notes](11-1-3/)
## 11.1.2
Release date: 2021-01-21

@ -17,6 +17,10 @@ In OpenProject EE, you can synchronize LDAP group memberships defined through th
- 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.
<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:
![ldap groups](ldap-groups-1-900x363@2x.png)

@ -4,7 +4,7 @@ sidebar_navigation:
priority: 990
description: Manage users in OpenProject.
robots: index, follow
keywords: manage users
keywords: manage users, lock, unlock, invite, language
---
# Manage Users
@ -17,9 +17,9 @@ The users list provides an overview of all users in OpenProject. You can create
| Topic | Content |
| --------------------------------------------- | ------------------------------------------------------------ |
| ----------------------------------------------- | ------------------------------------------------------------ |
| [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. |
| [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. |
@ -35,18 +35,21 @@ Also, you get the information when the user has been created, and when the user
![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 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">
**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>
![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
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).
## 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 any textile field you can reference revisions by putting an "r" in front of the revision number (e.g. r123).
## Configure Repositories in OpenProject
Please see our system admin guide [how to configure repositories in OpenProject](../../system-admin-guide/system-settings/repositories/).

@ -73,6 +73,10 @@ The results will be displayed accordingly in the work package list.
![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
### 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 {OpModalLocalsToken} from "core-components/op-modals/op-modal.service";
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 {Observable} from 'rxjs';
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 });
}
private handleError(error:string) {
this.notifications.addError(error || this.I18n.t('js.error.internal'));
private handleError(error:HttpErrorResponse) {
// 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) {

@ -1,5 +1,4 @@
<div class="textarea-wrapper">
<ng-select [(ngModel)]="selectedOption"
<ng-select [(ngModel)]="selectedOption"
[ngClass]="'inline-edit--field -multi-select'"
[required]="required"
[clearable]="!required"
@ -19,13 +18,12 @@
[appendTo]="appendTo"
[dropdownPosition]="'top'"
[hideSelected]="true">
</ng-select>
</ng-select>
<edit-field-controls [fieldController]="self"
<edit-field-controls [fieldController]="self"
*ngIf="!handler.inEditMode"
(onSave)="handler.handleUserSubmit()"
(onCancel)="handler.handleUserCancel()"
[saveTitle]="text.save"
[cancelTitle]="text.cancel">
</edit-field-controls>
</div>
</edit-field-controls>

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

@ -35,7 +35,10 @@ module API
FilterDependencyRepresenter
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
def type

@ -50,6 +50,7 @@ module OpenProject
# which will be uploaded directly to the cloud storage rather than via OpenProject's
# server process.
'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,
'log_level' => 'info',
'scm_git_command' => nil,

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

@ -43,8 +43,8 @@ uk:
invalid_statuses_found: 'Invalid status names found'
invalid_priorities_found: 'Invalid priority names found'
invalid_emails_found: 'Invalid email addresses found'
unknown_emails_found: 'Unknown email addresses found'
unknown_property: 'Unknown property'
unknown_emails_found: 'Знайдено невідомі адреси електронної пошти'
unknown_property: 'Невідома властивість'
non_members_found: 'Non project members found'
import_types_as: 'Set all these types 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(:request_path) { api_v3_paths.attachments_by_budget budget.id }
let(:request_parts) { { metadata: metadata, file: file } }
let(:metadata) { { fileName: 'cat.png' }.to_json }
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
@ -99,19 +99,19 @@ describe 'API v3 Attachments by budget resource', type: :request do
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, 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')
end
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'
end
context 'metadata is missing the fileName' do
let(:metadata) { Hash.new.to_json }
let(:metadata) { Hash.new }
it_behaves_like 'constraint violation' do
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(:request_path) { api_v3_paths.attachments_by_document document.id }
let(:request_parts) { { metadata: metadata, file: file } }
let(:metadata) { { fileName: 'cat.png' }.to_json }
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
@ -100,19 +100,19 @@ describe 'API v3 Attachments by document resource', type: :request do
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, 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')
end
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'
end
context 'metadata is missing the fileName' do
let(:metadata) { Hash.new.to_json }
let(:metadata) { Hash.new }
it_behaves_like 'constraint violation' do
let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" }

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

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

@ -242,4 +242,31 @@ describe 'work package export', type: :feature do
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

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

@ -47,7 +47,8 @@ describe ::API::V3::Queries::Schemas::ProjectFilterDependencyRepresenter, clear_
describe 'values' do
let(:path) { 'values' }
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
let(:operator) { Queries::Operators::Equals }

@ -318,7 +318,7 @@ describe Attachment, type: :model do
let(:url_options) { { expires_in: 1.year } }
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

@ -49,6 +49,29 @@ describe Group, type: :model do
expect(g.save).to eq true
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
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
allow(Project)
.to receive_message_chain(:visible, :exists?)
.to receive_message_chain(:visible, :active, :exists?)
.and_return(true)
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
allow(Project)
.to receive_message_chain(:visible, :exists?)
.to receive_message_chain(:visible, :active, :exists?)
.and_return(false)
expect(instance).to_not be_available
@ -71,7 +71,7 @@ describe Queries::WorkPackages::Filter::ProjectFilter, type: :model do
visible_projects = [parent, child]
allow(Project)
.to receive(:visible)
.to receive_message_chain(:visible, :active)
.and_return(visible_projects)
allow(Project)
@ -99,7 +99,7 @@ describe Queries::WorkPackages::Filter::ProjectFilter, type: :model do
before do
allow(Project)
.to receive(:visible)
.to receive_message_chain(:visible, :active)
.and_return([project, project2])
instance.values = [project.id.to_s]

@ -98,6 +98,34 @@ describe User, type: :model do
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
before do
user.login = login

@ -42,8 +42,8 @@ shared_examples 'it supports direct uploads' do
end
describe 'POST /prepare', with_settings: { attachment_max_size: 512 } do
let(:request_parts) { { metadata: metadata, file: file } }
let(:metadata) { { fileName: 'cat.png', fileSize: file.size }.to_json }
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') }
def request!
@ -68,7 +68,7 @@ shared_examples 'it supports direct uploads' do
end
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
expect(subject.status).to eq(422)
@ -125,8 +125,21 @@ shared_examples 'it supports direct uploads' do
"success_action_status"
)
expect(fields["Content-Type"]).to eq metadata[:contentType]
expect(fields["key"]).to end_with "cat.png"
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
@ -214,8 +227,8 @@ shared_examples 'an APIv3 attachment resource', type: :request, content_type: :j
let(:permissions) { Array(update_permission) }
let(:request_path) { api_v3_paths.attachments }
let(:request_parts) { { metadata: metadata, file: file } }
let(:metadata) { { fileName: 'cat.png' }.to_json }
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
@ -248,19 +261,19 @@ shared_examples 'an APIv3 attachment resource', type: :request, content_type: :j
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, 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')
end
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'
end
context 'metadata is missing the fileName' do
let(:metadata) { Hash.new.to_json }
let(:metadata) { Hash.new }
it_behaves_like 'constraint violation' do
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
let(:request_path) { api_v3_paths.send "attachments_by_#{attachment_type}", container.id }
let(:request_parts) { { metadata: metadata, file: file } }
let(:metadata) { { fileName: 'cat.png' }.to_json }
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
@ -527,19 +540,19 @@ shared_examples 'an APIv3 attachment resource', type: :request, content_type: :j
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, 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')
end
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'
end
context 'metadata is missing the fileName' do
let(:metadata) { Hash.new.to_json }
let(:metadata) { Hash.new }
it_behaves_like 'constraint violation' do
let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" }

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

@ -386,11 +386,8 @@ describe MailHandler, type: :model do
['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', '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', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
# ['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', '-']
['jsmith@example.net', 'AVeryLongFirstnameThatNoLongerExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatNoLongerExceedsTheMaximumLength', 'Smith'],
['jsmith@example.net', 'John AVeryLongLastnameThatNoLongerExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatNoLongerExceedsTheMaximumLength']
}
to_test.each do |attrs, expected|

Loading…
Cancel
Save