diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 548195018b..9589629cb4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -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 diff --git a/.ruby-version b/.ruby-version index 860487ca19..37c2961c24 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.1 +2.7.2 diff --git a/.travis.yml b/.travis.yml index 9c5450b24f..640b3be621 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ language: ruby rvm: - - 2.7.1 + - 2.7.2 sudo: required dist: xenial diff --git a/Gemfile b/Gemfile index c71a816429..2f9a94dd63 100644 --- a/Gemfile +++ b/Gemfile @@ -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' diff --git a/Gemfile.lock b/Gemfile.lock index 06c68660dd..5f98de482a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/helpers/versions_helper.rb b/app/helpers/versions_helper.rb index 3a483924b4..4e066603fa 100644 --- a/app/helpers/versions_helper.rb +++ b/app/helpers/versions_helper.rb @@ -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 diff --git a/app/models/group.rb b/app/models/group.rb index 9913ca005c..a0f683a359 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -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. diff --git a/app/models/queries/work_packages/filter/project_filter.rb b/app/models/queries/work_packages/filter/project_filter.rb index c64cca1c39..c858bc810f 100644 --- a/app/models/queries/work_packages/filter/project_filter.rb +++ b/app/models/queries/work_packages/filter/project_filter.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 482bc585d6..7634246024 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/models/work_package/exporter/csv.rb b/app/models/work_package/exporter/csv.rb index 7656b4611f..262d779e57 100644 --- a/app/models/work_package/exporter/csv.rb +++ b/app/models/work_package/exporter/csv.rb @@ -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 diff --git a/app/uploaders/direct_fog_uploader.rb b/app/uploaders/direct_fog_uploader.rb index 037a3cea8b..a40748d279 100644 --- a/app/uploaders/direct_fog_uploader.rb +++ b/app/uploaders/direct_fog_uploader.rb @@ -3,50 +3,79 @@ require_relative 'fog_file_uploader' class DirectFogUploader < FogFileUploader 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 - def self.for_uploader(fog_file_uploader) - raise ArgumentError, "FogFileUploader expected" unless fog_file_uploader.is_a? FogFileUploader + class << self + 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.instance_variable_set "@key", fog_file_uploader.path + uploader = self.new - uploader - end + 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 - ## - # Generates the direct upload form for the given attachment. - # - # @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). - # @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( - attachment:, - success_action_redirect: nil, - success_action_status: "201", - max_file_size: Setting.attachment_max_size * 1024 - ) - uploader = for_attachment attachment - - if success_action_redirect.present? - 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 + uploader + end + + ## + # Generates the direct upload form for the given attachment. + # + # @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). + # @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 direct_fog_hash( + attachment:, + success_action_redirect: nil, + success_action_status: "201", + max_file_size: Setting.attachment_max_size * 1024 + ) + 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? + 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 - 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? - hash.merge(success_action_redirect: success_action_redirect) - else - hash.merge(success_action_status: success_action_status) + def direct_fog_hash_uploader(attachment, success_action_redirect, success_action_status) + for_attachment(attachment).tap do |uploader| + if success_action_redirect.present? + 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 diff --git a/app/uploaders/fog_file_uploader.rb b/app/uploaders/fog_file_uploader.rb index ecff0bca84..bde1916211 100644 --- a/app/uploaders/fog_file_uploader.rb +++ b/app/uploaders/fog_file_uploader.rb @@ -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 diff --git a/app/views/versions/_roadmap_filter.html.erb b/app/views/versions/_roadmap_filter.html.erb index 981484787d..8420936c5b 100644 --- a/app/views/versions/_roadmap_filter.html.erb +++ b/app/views/versions/_roadmap_filter.html.erb @@ -21,7 +21,6 @@ <% if @project.descendants.active.any? %>
- <%= hidden_field_tag 'with_subprojects', 0 %>
<%= styled_label_tag "with-subprojects", t(:label_subproject_plural) %> diff --git a/app/views/versions/_roadmap_version_links.html.erb b/app/views/versions/_roadmap_version_links.html.erb index cbb415e44d..68c3b6814c 100644 --- a/app/views/versions/_roadmap_version_links.html.erb +++ b/app/views/versions/_roadmap_version_links.html.erb @@ -1,5 +1,8 @@

<%= t(:label_version_plural) %>

<% @versions.each do |version| %> - <%= link_to format_version_name(version), "#{project_roadmap_url}##{version.name}" %>
+ <%= 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)) %>
<% end %> diff --git a/app/views/versions/index.html.erb b/app/views/versions/index.html.erb index 974a2ae7cf..a7224a31eb 100644 --- a/app/views/versions/index.html.erb +++ b/app/views/versions/index.html.erb @@ -45,7 +45,7 @@ See docs/COPYRIGHT.rdoc for more details.
<% @versions.each do |version| %>

- <%= link_to_version version, name: h(version.name) %> + <%= link_to_version(version, name: h(version.name), id: "version-#{version.name}") %>

<%= render partial: 'versions/overview', locals: {version: version} %> <%= render(partial: "wiki/content", locals: {content: version.wiki_page.content}) if version.wiki_page %> diff --git a/config/locales/crowdin/js-tr.yml b/config/locales/crowdin/js-tr.yml index 592e00c8f9..b50a2dd516 100644 --- a/config/locales/crowdin/js-tr.yml +++ b/config/locales/crowdin/js-tr.yml @@ -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' diff --git a/db/migrate/20210127134438_alter_user_attributes_max_length.rb b/db/migrate/20210127134438_alter_user_attributes_max_length.rb new file mode 100644 index 0000000000..58cacd3b33 --- /dev/null +++ b/db/migrate/20210127134438_alter_user_attributes_max_length.rb @@ -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 diff --git a/docker/dev/backend/Dockerfile b/docker/dev/backend/Dockerfile index 6da9945ee6..0061587acb 100644 --- a/docker/dev/backend/Dockerfile +++ b/docker/dev/backend/Dockerfile @@ -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 diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index 6d3d85e2c0..c438633667 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -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 diff --git a/docs/api/apiv3/endpoints/attachments.apib b/docs/api/apiv3/endpoints/attachments.apib index e2edceb74d..6e1ca66d2b 100644 --- a/docs/api/apiv3/endpoints/attachments.apib +++ b/docs/api/apiv3/endpoints/attachments.apib @@ -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" diff --git a/docs/api/apiv3/endpoints/members.apib b/docs/api/apiv3/endpoints/members.apib index 332f8684a1..f8bf53e19c 100644 --- a/docs/api/apiv3/endpoints/members.apib +++ b/docs/api/apiv3/endpoints/members.apib @@ -113,20 +113,22 @@ You can use the form and schema to be retrieve the valid attribute values and by + Body { - "project": { - "href": "/api/v3/projects/1" - }, - "principal": { - "href": "/api/v3/users/5" - }, - "roles": [ - { - "href": "/api/v3/roles/5" - }, - { - "href": "/api/v3/roles/8" + "_links": { + "project": { + "href": "/api/v3/projects/1" + }, + "principal": { + "href": "/api/v3/users/5" + }, + "roles": [ + { + "href": "/api/v3/roles/5" + }, + { + "href": "/api/v3/roles/8" + } + ] } - ] } + Response 201 diff --git a/docs/api/apiv3/forms.apib b/docs/api/apiv3/forms.apib index 6a4df00906..034ce2d7a1 100644 --- a/docs/api/apiv3/forms.apib +++ b/docs/api/apiv3/forms.apib @@ -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: diff --git a/docs/api/apiv3/index.apib b/docs/api/apiv3/index.apib index fb3e594dcb..577ecd185f 100644 --- a/docs/api/apiv3/index.apib +++ b/docs/api/apiv3/index.apib @@ -1,7 +1,8 @@ - + + @@ -11,7 +12,6 @@ - diff --git a/docs/development/development-environment-osx/README.md b/docs/development/development-environment-osx/README.md index ba0dc50615..9a7a4a6d41 100644 --- a/docs/development/development-environment-osx/README.md +++ b/docs/development/development-environment-osx/README.md @@ -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 diff --git a/docs/development/development-environment-ubuntu/README.md b/docs/development/development-environment-ubuntu/README.md index e19fc76189..e804c9f2e6 100644 --- a/docs/development/development-environment-ubuntu/README.md +++ b/docs/development/development-environment-ubuntu/README.md @@ -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 diff --git a/docs/installation-and-operations/changing-to-bim-edition/README.md b/docs/installation-and-operations/changing-to-bim-edition/README.md index c30c0158b5..deef3d3ea0 100644 --- a/docs/installation-and-operations/changing-to-bim-edition/README.md +++ b/docs/installation-and-operations/changing-to-bim-edition/README.md @@ -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 diff --git a/docs/installation-and-operations/configuration/README.md b/docs/installation-and-operations/configuration/README.md index c8af4b70bc..d47c8e23d1 100644 --- a/docs/installation-and-operations/configuration/README.md +++ b/docs/installation-and-operations/configuration/README.md @@ -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 diff --git a/docs/installation-and-operations/configuration/ssl/README.md b/docs/installation-and-operations/configuration/ssl/README.md index 6b164379f1..5743f900ac 100644 --- a/docs/installation-and-operations/configuration/ssl/README.md +++ b/docs/installation-and-operations/configuration/ssl/README.md @@ -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 externally1 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'` + + +_1 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._ diff --git a/docs/installation-and-operations/installation/manual/README.md b/docs/installation-and-operations/installation/manual/README.md index 5cd78d8d02..9b07dbce57 100644 --- a/docs/installation-and-operations/installation/manual/README.md +++ b/docs/installation-and-operations/installation/manual/README.md @@ -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 diff --git a/docs/release-notes/11-1-3/README.md b/docs/release-notes/11-1-3/README.md new file mode 100644 index 0000000000..ead072b84b --- /dev/null +++ b/docs/release-notes/11-1-3/README.md @@ -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. + + +#### 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 diff --git a/docs/release-notes/README.md b/docs/release-notes/README.md index 3f6e1cab69..0bc39283ed 100644 --- a/docs/release-notes/README.md +++ b/docs/release-notes/README.md @@ -12,6 +12,13 @@ Stay up to date and get an overview of the new features included in the releases +## 11.1.3 + +Release date: 2021-02-11 + +[Release Notes](11-1-3/) + + ## 11.1.2 Release date: 2021-01-21 diff --git a/docs/system-admin-guide/authentication/ldap-authentication/ldap-group-synchronization/README.md b/docs/system-admin-guide/authentication/ldap-authentication/ldap-group-synchronization/README.md index 7d1945c283..c4ec1c53b8 100644 --- a/docs/system-admin-guide/authentication/ldap-authentication/ldap-group-synchronization/README.md +++ b/docs/system-admin-guide/authentication/ldap-authentication/ldap-group-synchronization/README.md @@ -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 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. + + For the sake of simplicity, we assume that in this guide, your LDAP structure looks like the following: diff --git a/docs/system-admin-guide/users-permissions/users/README.md b/docs/system-admin-guide/users-permissions/users/README.md index dbaacf7bf7..d59fd0197a 100644 --- a/docs/system-admin-guide/users-permissions/users/README.md +++ b/docs/system-admin-guide/users-permissions/users/README.md @@ -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 @@ -16,14 +16,14 @@ 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. | -| [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. | -| [Delete users](#delete-users) | Delete a user from the system. | +| Topic | Content | +| ----------------------------------------------- | ------------------------------------------------------------ | +| [User list](#user-list) | Manage all users in OpenProject. | +| [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. | +| [Delete users](#delete-users) | Delete a user from the system. | ## 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) -## 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. ![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. diff --git a/docs/user-guide/repository/README.md b/docs/user-guide/repository/README.md index 67af4e4be0..d2c4e46a73 100644 --- a/docs/user-guide/repository/README.md +++ b/docs/user-guide/repository/README.md @@ -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/). @@ -55,4 +60,4 @@ Please see our system admin guide [how to configure repositories in OpenProject] ## Repository integration -See our Installation and operations guide how to [integrate repositories in Openproject](../../installation-and-operations/configuration/repositories/#repository-integration-in-openproject). \ No newline at end of file +See our Installation and operations guide how to [integrate repositories in Openproject](../../installation-and-operations/configuration/repositories/#repository-integration-in-openproject). diff --git a/docs/user-guide/work-packages/work-package-table-configuration/README.md b/docs/user-guide/work-packages/work-package-table-configuration/README.md index b327d68086..6d3c69d046 100644 --- a/docs/user-guide/work-packages/work-package-table-configuration/README.md +++ b/docs/user-guide/work-packages/work-package-table-configuration/README.md @@ -73,6 +73,10 @@ The results will be displayed accordingly in the work package list. ![filter-text](filter-text.png) + + ## Sort the work package list ### Automatic sorting of the work package list diff --git a/frontend/src/app/components/modals/export-modal/wp-table-export.modal.ts b/frontend/src/app/components/modals/export-modal/wp-table-export.modal.ts index 8d5e650f8b..0df2460b2a 100644 --- a/frontend/src/app/components/modals/export-modal/wp-table-export.modal.ts +++ b/frontend/src/app/components/modals/export-modal/wp-table-export.modal.ts @@ -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) { diff --git a/frontend/src/app/modules/fields/edit/field-types/multi-select-edit-field.component.html b/frontend/src/app/modules/fields/edit/field-types/multi-select-edit-field.component.html index 8f55882471..6687e7d1f7 100644 --- a/frontend/src/app/modules/fields/edit/field-types/multi-select-edit-field.component.html +++ b/frontend/src/app/modules/fields/edit/field-types/multi-select-edit-field.component.html @@ -1,31 +1,29 @@ -
- - + + - - -
+ + diff --git a/frontend/src/app/modules/job-status/job-status-modal/job-status.modal.html b/frontend/src/app/modules/job-status/job-status-modal/job-status.modal.html index 7e87bcd58a..fe45cd1a84 100644 --- a/frontend/src/app/modules/job-status/job-status-modal/job-status.modal.html +++ b/frontend/src/app/modules/job-status/job-status-modal/job-status.modal.html @@ -25,6 +25,7 @@ {{ text.download_starts }} diff --git a/lib/api/v3/queries/schemas/project_filter_dependency_representer.rb b/lib/api/v3/queries/schemas/project_filter_dependency_representer.rb index b8b768d925..398ad22e63 100644 --- a/lib/api/v3/queries/schemas/project_filter_dependency_representer.rb +++ b/lib/api/v3/queries/schemas/project_filter_dependency_representer.rb @@ -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 diff --git a/lib/open_project/configuration.rb b/lib/open_project/configuration.rb index 63b664b6b7..ed240d13f8 100644 --- a/lib/open_project/configuration.rb +++ b/lib/open_project/configuration.rb @@ -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, diff --git a/lib/open_project/version.rb b/lib/open_project/version.rb index eee4238ee9..01aea555e0 100644 --- a/lib/open_project/version.rb +++ b/lib/open_project/version.rb @@ -34,7 +34,7 @@ module OpenProject module VERSION #:nodoc: MAJOR = 11 MINOR = 1 - PATCH = 2 + PATCH = 3 TINY = PATCH # Redmine compat class << self diff --git a/modules/bim/config/locales/crowdin/uk.yml b/modules/bim/config/locales/crowdin/uk.yml index 7ec9d2ed68..33a9a86438 100644 --- a/modules/bim/config/locales/crowdin/uk.yml +++ b/modules/bim/config/locales/crowdin/uk.yml @@ -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' diff --git a/modules/costs/spec/requests/api/attachments/attachments_by_budget_resource_spec.rb b/modules/costs/spec/requests/api/attachments/attachments_by_budget_resource_spec.rb index 911c3958c9..207eb52a2a 100644 --- a/modules/costs/spec/requests/api/attachments/attachments_by_budget_resource_spec.rb +++ b/modules/costs/spec/requests/api/attachments/attachments_by_budget_resource_spec.rb @@ -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')}" } diff --git a/modules/documents/spec/requests/api/v3/attachments/attachments_by_documents_resource_spec.rb b/modules/documents/spec/requests/api/v3/attachments/attachments_by_documents_resource_spec.rb index 5d842fb594..9d331d7926 100644 --- a/modules/documents/spec/requests/api/v3/attachments/attachments_by_documents_resource_spec.rb +++ b/modules/documents/spec/requests/api/v3/attachments/attachments_by_documents_resource_spec.rb @@ -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')}" } diff --git a/modules/ldap_groups/app/models/ldap_groups/synchronized_group.rb b/modules/ldap_groups/app/models/ldap_groups/synchronized_group.rb index 5720516c16..e6a23fda46 100644 --- a/modules/ldap_groups/app/models/ldap_groups/synchronized_group.rb +++ b/modules/ldap_groups/app/models/ldap_groups/synchronized_group.rb @@ -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 diff --git a/modules/ldap_groups/lib/open_project/ldap_groups/synchronize_filter.rb b/modules/ldap_groups/lib/open_project/ldap_groups/synchronize_filter.rb index 9e3f897d82..42b27a8bd9 100644 --- a/modules/ldap_groups/lib/open_project/ldap_groups/synchronize_filter.rb +++ b/modules/ldap_groups/lib/open_project/ldap_groups/synchronize_filter.rb @@ -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 diff --git a/spec/features/work_packages/export_spec.rb b/spec/features/work_packages/export_spec.rb index ed43210cdb..0e8e6a653b 100644 --- a/spec/features/work_packages/export_spec.rb +++ b/spec/features/work_packages/export_spec.rb @@ -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 diff --git a/spec/helpers/versions_helper_spec.rb b/spec/helpers/versions_helper_spec.rb index e566bc846c..dab981f94f 100644 --- a/spec/helpers/versions_helper_spec.rb +++ b/spec/helpers/versions_helper_spec.rb @@ -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("#{test_project.name} - #{version.name}") + expect(link_to_version(version)) + .to be_html_eql("#{test_project.name} - #{version.name}") end it 'generates a link within a project' do @project = test_project - expect(link_to_version(version)).to eq("#{version.name}") + expect(link_to_version(version)) + .to be_html_eql("#{version.name}") 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 diff --git a/spec/lib/api/v3/queries/schemas/project_filter_dependency_representer_spec.rb b/spec/lib/api/v3/queries/schemas/project_filter_dependency_representer_spec.rb index 93ce5b6a03..9371d24cd6 100644 --- a/spec/lib/api/v3/queries/schemas/project_filter_dependency_representer_spec.rb +++ b/spec/lib/api/v3/queries/schemas/project_filter_dependency_representer_spec.rb @@ -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 } diff --git a/spec/models/attachment_spec.rb b/spec/models/attachment_spec.rb index 4993ad7944..6dc4c1971e 100644 --- a/spec/models/attachment_spec.rb +++ b/spec/models/attachment_spec.rb @@ -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 diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 5f6fe622c5..031f963749 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -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 } diff --git a/spec/models/queries/work_packages/filter/project_filter_instance_spec.rb b/spec/models/queries/work_packages/filter/project_filter_instance_spec.rb new file mode 100644 index 0000000000..a1427c31ec --- /dev/null +++ b/spec/models/queries/work_packages/filter/project_filter_instance_spec.rb @@ -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 diff --git a/spec/models/queries/work_packages/filter/project_filter_spec.rb b/spec/models/queries/work_packages/filter/project_filter_spec.rb index 3864298a57..da33b2b003 100644 --- a/spec/models/queries/work_packages/filter/project_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/project_filter_spec.rb @@ -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] diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c76fde65ea..1a967ce1c0 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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 diff --git a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb index cf4a6fbb82..1f3a1af56c 100644 --- a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb +++ b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb @@ -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')}" } diff --git a/spec/requests/api/v3/attachments_spec.rb b/spec/requests/api/v3/attachments_spec.rb index 3940f03423..dd1f7c4eb1 100644 --- a/spec/requests/api/v3/attachments_spec.rb +++ b/spec/requests/api/v3/attachments_spec.rb @@ -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 diff --git a/spec_legacy/unit/mail_handler_spec.rb b/spec_legacy/unit/mail_handler_spec.rb index ee4e756c73..a880dffff2 100644 --- a/spec_legacy/unit/mail_handler_spec.rb +++ b/spec_legacy/unit/mail_handler_spec.rb @@ -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|