From ee4b50995dbb16f26938b5f5edef64679c9d98aa Mon Sep 17 00:00:00 2001 From: Wieland Lindenthal Date: Thu, 24 Mar 2022 17:12:39 +0100 Subject: [PATCH 01/60] Seed storages permissions https://community.openproject.org/wp/41529 --- modules/storage/spec/seeders/seeder_spec.rb | 40 +++++++++++++++++++ .../lib/open_project/storages/engine.rb | 2 + .../storages/patches/role_seeder_patch.rb | 31 ++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 modules/storage/spec/seeders/seeder_spec.rb create mode 100644 modules/storages/lib/open_project/storages/patches/role_seeder_patch.rb diff --git a/modules/storage/spec/seeders/seeder_spec.rb b/modules/storage/spec/seeders/seeder_spec.rb new file mode 100644 index 0000000000..1b6c455f01 --- /dev/null +++ b/modules/storage/spec/seeders/seeder_spec.rb @@ -0,0 +1,40 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2022 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' + +describe RootSeeder, + 'Storage module' do + it 'seeds role permissions for Storages' do + expect { described_class.new.do_seed! }.not_to raise_error + + expect(RolePermission.where(permission: :view_file_links).count).to eq 5 + expect(RolePermission.where(permission: :manage_file_links).count).to eq 2 + expect(RolePermission.where(permission: :manage_storages_in_project).count).to eq 1 + end +end diff --git a/modules/storages/lib/open_project/storages/engine.rb b/modules/storages/lib/open_project/storages/engine.rb index 9a3c5e7e54..30d2ba02aa 100644 --- a/modules/storages/lib/open_project/storages/engine.rb +++ b/modules/storages/lib/open_project/storages/engine.rb @@ -79,6 +79,8 @@ module OpenProject::Storages parent: :settings end + patch_with_namespace :BasicData, :RoleSeeder + # This hook is executed when the module is loaded. config.to_prepare do # We have a bunch of filters defined within the module. Here we register the filters. diff --git a/modules/storages/lib/open_project/storages/patches/role_seeder_patch.rb b/modules/storages/lib/open_project/storages/patches/role_seeder_patch.rb new file mode 100644 index 0000000000..e5d9d7ff5e --- /dev/null +++ b/modules/storages/lib/open_project/storages/patches/role_seeder_patch.rb @@ -0,0 +1,31 @@ +module OpenProject::Storages::Patches::RoleSeederPatch + def self.included(base) # :nodoc: + base.prepend InstanceMethods + end + + module InstanceMethods + def member + role_data = super + role_data[:permissions] += %i[view_file_links manage_file_links] + role_data + end + + def reader + role_data = super + role_data[:permissions] += %i[view_file_links] + role_data + end + + def non_member + role_data = super + role_data[:permissions] += %i[view_file_links] + role_data + end + + def anonymous + role_data = super + role_data[:permissions] += %i[view_file_links] + role_data + end + end +end From fa150d44e16557dfa3b4d5c35015f923900ced21 Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Fri, 25 Mar 2022 16:11:57 +0100 Subject: [PATCH 02/60] [bugfix] Added additional visibility check for file links - returning file link collections and single file links no longer return file links linking a work package within a project, which has no storage mapping to the file link's storage - filter on work package collections no longer return file links, that have the same mapping problem as above - added additional tests and test data to cover that behaviour --- .../filter/file_link_origin_id_filter.rb | 8 ++++++++ .../work_packages/filter/storage_id_filter.rb | 8 ++++++++ .../filter/storage_url_filter.rb | 6 +++++- .../filter/storages_filter_mixin.rb | 5 +++++ .../storages/app/models/storages/file_link.rb | 3 ++- .../api/v3/file_links/file_links_spec.rb | 18 ++++++++++++++--- .../work_packages_linked_filter_spec.rb | 20 +++++++++++++++++++ 7 files changed, 63 insertions(+), 5 deletions(-) diff --git a/modules/storages/app/models/queries/storages/work_packages/filter/file_link_origin_id_filter.rb b/modules/storages/app/models/queries/storages/work_packages/filter/file_link_origin_id_filter.rb index 2578997a6b..d1a3c18213 100644 --- a/modules/storages/app/models/queries/storages/work_packages/filter/file_link_origin_id_filter.rb +++ b/modules/storages/app/models/queries/storages/work_packages/filter/file_link_origin_id_filter.rb @@ -41,5 +41,13 @@ module Queries::Storages::WorkPackages::Filter def permission :view_file_links end + + def joins + %i[file_links storages] + end + + def additional_where_condition + "AND #{::Storages::FileLink.table_name}.storage_id = #{::Storages::Storage.table_name}.id" + end end end diff --git a/modules/storages/app/models/queries/storages/work_packages/filter/storage_id_filter.rb b/modules/storages/app/models/queries/storages/work_packages/filter/storage_id_filter.rb index b3a24593cf..f0733a41b7 100644 --- a/modules/storages/app/models/queries/storages/work_packages/filter/storage_id_filter.rb +++ b/modules/storages/app/models/queries/storages/work_packages/filter/storage_id_filter.rb @@ -41,5 +41,13 @@ module Queries::Storages::WorkPackages::Filter def permission :view_file_links end + + def joins + [:file_links, { project: :projects_storages }] + end + + def additional_where_condition + "AND #{::Storages::FileLink.table_name}.storage_id = #{::Storages::ProjectStorage.table_name}.storage_id" + end end end diff --git a/modules/storages/app/models/queries/storages/work_packages/filter/storage_url_filter.rb b/modules/storages/app/models/queries/storages/work_packages/filter/storage_url_filter.rb index 56de183c69..12b2c46419 100644 --- a/modules/storages/app/models/queries/storages/work_packages/filter/storage_url_filter.rb +++ b/modules/storages/app/models/queries/storages/work_packages/filter/storage_url_filter.rb @@ -47,7 +47,11 @@ module Queries::Storages::WorkPackages::Filter end def joins - { file_links: :storage } + [{ file_links: :storage }, { project: :projects_storages }] + end + + def additional_where_condition + "AND #{::Storages::FileLink.table_name}.storage_id = #{::Storages::ProjectStorage.table_name}.storage_id" end end end diff --git a/modules/storages/app/models/queries/storages/work_packages/filter/storages_filter_mixin.rb b/modules/storages/app/models/queries/storages/work_packages/filter/storages_filter_mixin.rb index 69b26ca71e..7bffb8123e 100644 --- a/modules/storages/app/models/queries/storages/work_packages/filter/storages_filter_mixin.rb +++ b/modules/storages/app/models/queries/storages/work_packages/filter/storages_filter_mixin.rb @@ -55,6 +55,7 @@ module Queries::Storages::WorkPackages::Filter::StoragesFilterMixin <<-SQL.squish #{::Queries::Operators::Equals.sql_for_field(where_values, filter_model.table_name, filter_column)} AND work_packages.project_id IN (#{Project.allowed_to(User.current, permission).select(:id).to_sql}) + #{additional_where_condition} SQL end @@ -62,6 +63,10 @@ module Queries::Storages::WorkPackages::Filter::StoragesFilterMixin values end + def additional_where_condition + '' + end + def joins filter_model.table_name.to_sym end diff --git a/modules/storages/app/models/storages/file_link.rb b/modules/storages/app/models/storages/file_link.rb index d411fc7091..79317938a9 100644 --- a/modules/storages/app/models/storages/file_link.rb +++ b/modules/storages/app/models/storages/file_link.rb @@ -64,9 +64,10 @@ class Storages::FileLink < ApplicationRecord # join projects through the container, and filter on projects visible from # the user includes(:container) - .includes(container: :project) + .includes(container: { project: :projects_storages }) .references(:projects) .merge(Project.allowed_to(user, :view_file_links)) + .where('projects_storages.storage_id = file_links.storage_id') } delegate :project, to: :container diff --git a/modules/storages/spec/requests/api/v3/file_links/file_links_spec.rb b/modules/storages/spec/requests/api/v3/file_links/file_links_spec.rb index 119b6dbca1..17a0ab2617 100644 --- a/modules/storages/spec/requests/api/v3/file_links/file_links_spec.rb +++ b/modules/storages/spec/requests/api/v3/file_links/file_links_spec.rb @@ -50,9 +50,14 @@ describe 'API v3 file links resource', type: :request do let(:file_link) do create(:file_link, creator: current_user, container: work_package, storage: storage) end - let(:another_file_link) do + let(:file_link_of_other_work_package) do create(:file_link, creator: current_user, container: another_work_package, storage: storage) end + # If a storage mapping between a project and a storage is removed, the file link still persist. This can occur on + # moving a work package to another project, too, if target project does not yet have the storage mapping. + let(:file_link_of_unlinked_storage) do + create(:file_link, creator: current_user, container: work_package, storage: another_storage) + end subject(:response) { last_response } @@ -65,7 +70,8 @@ describe 'API v3 file links resource', type: :request do before do file_link - another_file_link + file_link_of_other_work_package + file_link_of_unlinked_storage get path end @@ -312,11 +318,17 @@ describe 'API v3 file links resource', type: :request do it_behaves_like 'not found' end - context 'if no storage with that id exists' do + context 'if no file link with that id exists' do let(:path) { api_v3_paths.file_link(1337) } it_behaves_like 'not found' end + + context 'if file link is in a work package, while its project is not mapped to the file link\'s storage.' do + let(:path) { api_v3_paths.file_link(file_link_of_unlinked_storage.id) } + + it_behaves_like 'not found' + end end describe 'DELETE /api/v3/file_links/:file_link_id' do diff --git a/modules/storages/spec/requests/api/v3/work_packages/work_packages_linked_filter_spec.rb b/modules/storages/spec/requests/api/v3/work_packages/work_packages_linked_filter_spec.rb index df8992c633..b3577eb61c 100644 --- a/modules/storages/spec/requests/api/v3/work_packages/work_packages_linked_filter_spec.rb +++ b/modules/storages/spec/requests/api/v3/work_packages/work_packages_linked_filter_spec.rb @@ -63,6 +63,9 @@ describe 'API v3 work packages resource with filters for linked storage file', storage: storage2, origin_id: file_link1.origin_id) end + # This link is considered invisible, as it is linking a work package to a file, where the work package's project + # and the file's storage are not linked together. + let(:file_link4) { create(:file_link, creator: current_user, container: work_package3, storage: storage1) } subject(:response) { last_response } @@ -73,6 +76,7 @@ describe 'API v3 work packages resource with filters for linked storage file', file_link1 file_link2 file_link3 + file_link4 login_as current_user end @@ -116,6 +120,14 @@ describe 'API v3 work packages resource with filters for linked storage file', let(:elements) { [] } end end + + context 'if the filter is set to a file linked to a work package in an unlinked project' do + let(:origin_id_value) { file_link4.origin_id.to_s } + + it_behaves_like 'API V3 collection response', 0, 0, 'WorkPackage', 'WorkPackageCollection' do + let(:elements) { [] } + end + end end context 'with single filter for storage id' do @@ -169,6 +181,14 @@ describe 'API v3 work packages resource with filters for linked storage file', let(:elements) { [work_package3] } end + context 'if any of the matching work packages is in a project without a mapping to that storage' do + let(:storage_url_value) { CGI.escape(storage1.host) } + + it_behaves_like 'API V3 collection response', 2, 2, 'WorkPackage', 'WorkPackageCollection' do + let(:elements) { [work_package1, work_package2] } + end + end + context 'if one project has not sufficient permissions' do let(:role2) { create :role, permissions: %i(view_work_packages) } From fd7a26c6d7aa9e53b3656e3ffc206690105d9f55 Mon Sep 17 00:00:00 2001 From: Wieland Lindenthal Date: Fri, 25 Mar 2022 15:21:12 +0100 Subject: [PATCH 03/60] [#41534] Deletion of projects fail if project has views. https://community.openproject.org/work_packages/41534 --- app/models/project.rb | 2 +- spec/models/project_spec.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 85ebb7d78a..59c6407dff 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -71,7 +71,7 @@ class Project < ApplicationRecord }, dependent: :destroy has_many :time_entries, dependent: :delete_all has_many :time_entry_activities_projects, dependent: :delete_all - has_many :queries, dependent: :delete_all + has_many :queries, dependent: :destroy has_many :news, -> { includes(:author) }, dependent: :destroy has_many :categories, -> { order("#{Category.table_name}.name") }, dependent: :delete_all has_many :forums, -> { order('position ASC') }, dependent: :destroy diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index ba6bfc196b..f69afb23c8 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -178,4 +178,17 @@ describe Project, type: :model do expect(project.types_used_by_work_packages).to match_array [project_work_package.type] end end + + describe 'Views belonging to queries that belong to the project' do + let(:query) { create(:query, project: project) } + let(:view) { create(:view, query: query) } + + it 'destroys the views and queries when project gets destroyed' do + view + project.destroy + + expect { query.reload }.to raise_error ActiveRecord::RecordNotFound + expect { view.reload }.to raise_error ActiveRecord::RecordNotFound + end + end end From 5019a6e187bd2ec73edc9eb1cb337a3c83bdadc7 Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Tue, 29 Mar 2022 13:31:11 +0200 Subject: [PATCH 04/60] [chore] added drag open behvior for tabs --- .../tabs/global-search-tabs.component.ts | 13 +++-- .../content-tabs/content-tabs.component.ts | 4 +- .../scrollable-tabs.component.html | 4 +- .../scrollable-tabs.component.sass | 4 ++ .../scrollable-tabs.component.ts | 56 ++++++++++++++++++- 5 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.sass diff --git a/frontend/src/app/core/global_search/tabs/global-search-tabs.component.ts b/frontend/src/app/core/global_search/tabs/global-search-tabs.component.ts index efdd815c4f..fa4520759b 100644 --- a/frontend/src/app/core/global_search/tabs/global-search-tabs.component.ts +++ b/frontend/src/app/core/global_search/tabs/global-search-tabs.component.ts @@ -31,7 +31,10 @@ import { Component, OnDestroy, Injector, + OnInit, + ChangeDetectionStrategy, } from '@angular/core'; +import { StateService } from '@uirouter/core'; import { Subscription } from 'rxjs'; import { GlobalSearchService } from 'core-app/core/global_search/services/global-search.service'; import { ScrollableTabsComponent } from 'core-app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component'; @@ -42,9 +45,10 @@ export const globalSearchTabsSelector = 'global-search-tabs'; @Component({ selector: globalSearchTabsSelector, templateUrl: '../../../shared/components/tabs/scrollable-tabs/scrollable-tabs.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class GlobalSearchTabsComponent extends ScrollableTabsComponent implements OnDestroy { +export class GlobalSearchTabsComponent extends ScrollableTabsComponent implements OnInit, OnDestroy { private currentTabSub:Subscription; private tabsSub:Subscription; @@ -53,13 +57,14 @@ export class GlobalSearchTabsComponent extends ScrollableTabsComponent implement constructor( readonly globalSearchService:GlobalSearchService, + protected readonly $state:StateService, public injector:Injector, cdRef:ChangeDetectorRef, ) { - super(cdRef, injector); + super($state, cdRef, injector); } - ngOnInit() { + ngOnInit():void { this.currentTabSub = this.globalSearchService .currentTab$ .subscribe((currentTab) => { @@ -74,7 +79,7 @@ export class GlobalSearchTabsComponent extends ScrollableTabsComponent implement }); } - public clickTab(tab:TabDefinition, event:Event) { + public clickTab(tab:TabDefinition, event:Event):void { super.clickTab(tab, event); this.globalSearchService.currentTab = tab.id; diff --git a/frontend/src/app/shared/components/tabs/content-tabs/content-tabs.component.ts b/frontend/src/app/shared/components/tabs/content-tabs/content-tabs.component.ts index ba0999535f..0d552e0c76 100644 --- a/frontend/src/app/shared/components/tabs/content-tabs/content-tabs.component.ts +++ b/frontend/src/app/shared/components/tabs/content-tabs/content-tabs.component.ts @@ -58,13 +58,13 @@ export class ContentTabsComponent extends ScrollableTabsComponent { constructor( readonly elementRef:ElementRef, - readonly $state:StateService, + protected readonly $state:StateService, readonly gon:GonService, cdRef:ChangeDetectorRef, readonly I18n:I18nService, public injector:Injector, ) { - super(cdRef, injector); + super($state, cdRef, injector); const gonTabs = JSON.parse((this.gon.get('contentTabs') as any).tabs); const currentTab = JSON.parse((this.gon.get('contentTabs') as any).selected); diff --git a/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.html b/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.html index e3e46bbb9e..53d84ace64 100644 --- a/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.html +++ b/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.html @@ -3,10 +3,12 @@ #scrollContainer class="op-scrollable-tabs--tab-container" > -
    +
    • { + this.currentTabId = tab.id; + this.tabSelected.emit(tab); + + const route = this.$state.includes('**.details.*') + ? this.$state.$current.name + : tab.route; + + if (route) { + void this.$state.go(route, tab.routeParams as RawParams); + } + + this.debouncedTabActivationTimeout = null; + }, 300); + } + + public cancelDebouncedTabActivation():void { + this.dragTargetStack -= 1; + + if (this.dragTargetStack === 0) { + this.clearDebouncedTabActivation(); + } + } + + private clearDebouncedTabActivation():void { + if (this.debouncedTabActivationTimeout !== null) { + clearTimeout(this.debouncedTabActivationTimeout); + this.debouncedTabActivationTimeout = null; + } + } + + public onScroll():void { this.determineScrollButtonVisibility(); } From d1a738ca6a147cd3bf83242dd5dfbaeccce3bd03 Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Wed, 30 Mar 2022 10:47:48 +0200 Subject: [PATCH 05/60] [bugfix] add spec for projects without storages module --- .../api/v3/file_links/file_links_spec.rb | 30 ++++++++++++++----- .../work_packages_linkable_filter_spec.rb | 16 ++++++++++ .../work_packages_linked_filter_spec.rb | 8 +++++ spec/factories/project_factory.rb | 6 ++-- 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/modules/storages/spec/requests/api/v3/file_links/file_links_spec.rb b/modules/storages/spec/requests/api/v3/file_links/file_links_spec.rb index 17a0ab2617..41d7663647 100644 --- a/modules/storages/spec/requests/api/v3/file_links/file_links_spec.rb +++ b/modules/storages/spec/requests/api/v3/file_links/file_links_spec.rb @@ -86,6 +86,14 @@ describe 'API v3 file links resource', type: :request do let(:elements) { [] } end end + + context 'if storages module is deactivated for the work package\'s project' do + let(:project) { create(:project, disable_modules: :storages) } + + it_behaves_like 'API V3 collection response', 0, 0, 'FileLink', 'Collection' do + let(:elements) { [] } + end + end end describe 'POST /api/v3/work_packages/:work_package_id/file_links' do @@ -168,15 +176,17 @@ describe 'API v3 file links resource', type: :request do context 'when some file link elements with matching origin_id, container, and storage already exist in database' do let(:existing_file_link) do - create(:file_link, origin_name: 'original name', - creator: current_user, - container: work_package, - storage: storage) + create(:file_link, + origin_name: 'original name', + creator: current_user, + container: work_package, + storage: storage) end let(:already_existing_file_link_payload) do - build(:file_link_element, origin_name: 'new name', - origin_id: existing_file_link.origin_id, - storage_url: existing_file_link.storage.host) + build(:file_link_element, + origin_name: 'new name', + origin_id: existing_file_link.origin_id, + storage_url: existing_file_link.storage.host) end let(:some_file_link_payload) do build(:file_link_element, storage_url: existing_file_link.storage.host) @@ -329,6 +339,12 @@ describe 'API v3 file links resource', type: :request do it_behaves_like 'not found' end + + context 'if file link is in a work package, while the storages module is deactivated in its project.' do + let(:project) { create(:project, disable_modules: :storages) } + + it_behaves_like 'not found' + end end describe 'DELETE /api/v3/file_links/:file_link_id' do diff --git a/modules/storages/spec/requests/api/v3/work_packages/work_packages_linkable_filter_spec.rb b/modules/storages/spec/requests/api/v3/work_packages/work_packages_linkable_filter_spec.rb index 57ea1d9fb4..0c8d7f415e 100644 --- a/modules/storages/spec/requests/api/v3/work_packages/work_packages_linkable_filter_spec.rb +++ b/modules/storages/spec/requests/api/v3/work_packages/work_packages_linkable_filter_spec.rb @@ -106,6 +106,14 @@ describe 'API v3 work packages resource with filters for the linkable to storage end end + context 'if a project has the storages module deactivated' do + let(:project1) { create(:project, disable_modules: :storages, members: { current_user => role1 }) } + + it_behaves_like 'API V3 collection response', 2, 2, 'WorkPackage', 'WorkPackageCollection' do + let(:elements) { [work_package3, work_package4] } + end + end + context 'if the filter is set to an unknown storage id' do let(:storage_id) { "1337" } @@ -140,6 +148,14 @@ describe 'API v3 work packages resource with filters for the linkable to storage end end + context 'if a project has the storages module deactivated' do + let(:project1) { create(:project, disable_modules: :storages, members: { current_user => role1 }) } + + it_behaves_like 'API V3 collection response', 2, 2, 'WorkPackage', 'WorkPackageCollection' do + let(:elements) { [work_package3, work_package4] } + end + end + context 'if the filter is set to an unknown storage url' do let(:storage_url) { CGI.escape("https://not.my-domain.org") } diff --git a/modules/storages/spec/requests/api/v3/work_packages/work_packages_linked_filter_spec.rb b/modules/storages/spec/requests/api/v3/work_packages/work_packages_linked_filter_spec.rb index b3577eb61c..69597929ff 100644 --- a/modules/storages/spec/requests/api/v3/work_packages/work_packages_linked_filter_spec.rb +++ b/modules/storages/spec/requests/api/v3/work_packages/work_packages_linked_filter_spec.rb @@ -113,6 +113,14 @@ describe 'API v3 work packages resource with filters for linked storage file', end end + context 'if a project has the storages module deactivated' do + let(:project1) { create(:project, disable_modules: :storages, members: { current_user => role1 }) } + + it_behaves_like 'API V3 collection response', 1, 1, 'WorkPackage', 'WorkPackageCollection' do + let(:elements) { [work_package3] } + end + end + context 'if the filter is set to an unknown file id from origin' do let(:origin_id_value) { "1337" } diff --git a/spec/factories/project_factory.rb b/spec/factories/project_factory.rb index 7beea9d8a7..aba405596e 100644 --- a/spec/factories/project_factory.rb +++ b/spec/factories/project_factory.rb @@ -36,14 +36,14 @@ FactoryBot.define do sequence(:name) { |n| "My Project No. #{n}" } sequence(:identifier) { |n| "myproject_no_#{n}" } - created_at { Time.now } - updated_at { Time.now } + created_at { Time.zone.now } + updated_at { Time.zone.now } enabled_module_names { OpenProject::AccessControl.available_project_modules } public { false } templated { false } callback(:after_build) do |project, evaluator| - disabled_modules = Array(evaluator.disable_modules) + disabled_modules = Array(evaluator.disable_modules).map(&:to_s) project.enabled_module_names = project.enabled_module_names - disabled_modules if !evaluator.no_types && project.types.empty? From b2f8f98c118eb65dcfa99240fcf02312e4ef7ae9 Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Wed, 30 Mar 2022 14:02:53 +0200 Subject: [PATCH 06/60] [chore] minor fixes to sass and spec files --- .../scrollable-tabs.component.sass | 7 ++--- .../attachments/attachment_upload_spec.rb | 30 ++++++++++++++++++- spec/support/browsers/firefox.rb | 4 +++ 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.sass b/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.sass index e6099ccd66..527f0218fb 100644 --- a/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.sass +++ b/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.sass @@ -1,4 +1,3 @@ -.op-tab-row--tab - .op-tab-row--link - * - pointer-events: none \ No newline at end of file +.op-tab-row--link + * + pointer-events: none diff --git a/spec/features/work_packages/attachments/attachment_upload_spec.rb b/spec/features/work_packages/attachments/attachment_upload_spec.rb index b05c8c5d1c..8188316031 100644 --- a/spec/features/work_packages/attachments/attachment_upload_spec.rb +++ b/spec/features/work_packages/attachments/attachment_upload_spec.rb @@ -1,3 +1,31 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2022 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + require 'spec_helper' require 'features/page_objects/notification' @@ -155,7 +183,7 @@ describe 'Upload attachment to work package', js: true do describe 'attachment dropzone' do it 'can upload an image via attaching and drag & drop' do - wp_page.switch_to_tab(tab: 'FILES') + wp_page.switch_to_tab(tab: 'files') container = page.find('.wp-attachment-upload') ## diff --git a/spec/support/browsers/firefox.rb b/spec/support/browsers/firefox.rb index f39ad3deaf..9f42c0c12c 100644 --- a/spec/support/browsers/firefox.rb +++ b/spec/support/browsers/firefox.rb @@ -32,6 +32,10 @@ def register_firefox(language, name: :"firefox_#{language}") options.args << "--headless" end + if ActiveRecord::Type::Boolean.new.cast(ENV['OPENPROJECT_TESTING_AUTO_DEVTOOLS']) + options.args << "--devtools" + end + is_grid = ENV['SELENIUM_GRID_URL'].present? driver_opts = { From f32ed57067a66f1a940f33541e4a1cc3196efb5a Mon Sep 17 00:00:00 2001 From: bsatarnejad Date: Wed, 30 Mar 2022 14:18:04 +0200 Subject: [PATCH 07/60] remove padding-left when there is hierarchy --- frontend/src/global_styles/layout/_main_menu.sass | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/global_styles/layout/_main_menu.sass b/frontend/src/global_styles/layout/_main_menu.sass index 6b3f02cf32..5221a869ff 100644 --- a/frontend/src/global_styles/layout/_main_menu.sass +++ b/frontend/src/global_styles/layout/_main_menu.sass @@ -317,6 +317,8 @@ a.main-menu--parent-node #main-menu ul ul.main-menu--children ul.pages-hierarchy .tree-menu--hierarchy-indicator color: var(--main-menu-font-color) + .tree-menu--hierarchy-span + a, a + padding-left: 0 .tree-menu--item &.-selected background: var(--main-menu-bg-selected-background) From b158302355e0b07f3f23c7a00ebc00290fe062af Mon Sep 17 00:00:00 2001 From: Behrokh Satarnejad <62008897+bsatarnejad@users.noreply.github.com> Date: Wed, 30 Mar 2022 15:51:12 +0200 Subject: [PATCH 08/60] change capitalization in team planner (#10405) --- config/locales/js-en.yml | 2 +- modules/team_planner/config/locales/js-en.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index cac0a762ba..17bfdf404f 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -1260,7 +1260,7 @@ en: next_button: 'Continue' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projects' clear_selection: 'Clear selection' apply: 'Apply' diff --git a/modules/team_planner/config/locales/js-en.yml b/modules/team_planner/config/locales/js-en.yml index 12c65e59e4..e7d9972da4 100644 --- a/modules/team_planner/config/locales/js-en.yml +++ b/modules/team_planner/config/locales/js-en.yml @@ -2,7 +2,7 @@ en: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' From d9fd7d0da0b49f835b03671498fa0acfc05e881f Mon Sep 17 00:00:00 2001 From: OpenProject Actions CI Date: Thu, 31 Mar 2022 03:15:11 +0000 Subject: [PATCH 09/60] update locales from crowdin [ci skip] --- config/locales/crowdin/js-af.yml | 2 +- config/locales/crowdin/js-ar.yml | 2 +- config/locales/crowdin/js-az.yml | 2 +- config/locales/crowdin/js-bg.yml | 2 +- config/locales/crowdin/js-ca.yml | 2 +- config/locales/crowdin/js-cs.yml | 2 +- config/locales/crowdin/js-da.yml | 2 +- config/locales/crowdin/js-de.yml | 2 +- config/locales/crowdin/js-el.yml | 2 +- config/locales/crowdin/js-eo.yml | 2 +- config/locales/crowdin/js-es.yml | 2 +- config/locales/crowdin/js-et.yml | 2 +- config/locales/crowdin/js-fa.yml | 2 +- config/locales/crowdin/js-fi.yml | 2 +- config/locales/crowdin/js-fil.yml | 2 +- config/locales/crowdin/js-fr.yml | 2 +- config/locales/crowdin/js-he.yml | 2 +- config/locales/crowdin/js-hi.yml | 2 +- config/locales/crowdin/js-hr.yml | 2 +- config/locales/crowdin/js-hu.yml | 2 +- config/locales/crowdin/js-id.yml | 2 +- config/locales/crowdin/js-it.yml | 2 +- config/locales/crowdin/js-ja.yml | 2 +- config/locales/crowdin/js-ko.yml | 2 +- config/locales/crowdin/js-lol.yml | 2 +- config/locales/crowdin/js-lt.yml | 2 +- config/locales/crowdin/js-lv.yml | 2 +- config/locales/crowdin/js-ne.yml | 2 +- config/locales/crowdin/js-nl.yml | 2 +- config/locales/crowdin/js-no.yml | 2 +- config/locales/crowdin/js-pl.yml | 2 +- config/locales/crowdin/js-pt.yml | 2 +- config/locales/crowdin/js-ro.yml | 2 +- config/locales/crowdin/js-ru.yml | 2 +- config/locales/crowdin/js-rw.yml | 2 +- config/locales/crowdin/js-si.yml | 2 +- config/locales/crowdin/js-sk.yml | 2 +- config/locales/crowdin/js-sl.yml | 2 +- config/locales/crowdin/js-sv.yml | 2 +- config/locales/crowdin/js-th.yml | 2 +- config/locales/crowdin/js-tr.yml | 2 +- config/locales/crowdin/js-uk.yml | 2 +- config/locales/crowdin/js-vi.yml | 2 +- config/locales/crowdin/js-zh-TW.yml | 2 +- config/locales/crowdin/lt.yml | 6 +- .../storages/config/locales/crowdin/lt.yml | 76 +++++++++---------- .../config/locales/crowdin/js-af.yml | 2 +- .../config/locales/crowdin/js-ar.yml | 2 +- .../config/locales/crowdin/js-az.yml | 2 +- .../config/locales/crowdin/js-bg.yml | 2 +- .../config/locales/crowdin/js-ca.yml | 2 +- .../config/locales/crowdin/js-cs.yml | 2 +- .../config/locales/crowdin/js-da.yml | 2 +- .../config/locales/crowdin/js-de.yml | 2 +- .../config/locales/crowdin/js-el.yml | 2 +- .../config/locales/crowdin/js-eo.yml | 2 +- .../config/locales/crowdin/js-es.yml | 2 +- .../config/locales/crowdin/js-et.yml | 2 +- .../config/locales/crowdin/js-fa.yml | 2 +- .../config/locales/crowdin/js-fi.yml | 2 +- .../config/locales/crowdin/js-fil.yml | 2 +- .../config/locales/crowdin/js-fr.yml | 2 +- .../config/locales/crowdin/js-he.yml | 2 +- .../config/locales/crowdin/js-hi.yml | 2 +- .../config/locales/crowdin/js-hr.yml | 2 +- .../config/locales/crowdin/js-hu.yml | 2 +- .../config/locales/crowdin/js-id.yml | 2 +- .../config/locales/crowdin/js-it.yml | 2 +- .../config/locales/crowdin/js-ja.yml | 2 +- .../config/locales/crowdin/js-ko.yml | 2 +- .../config/locales/crowdin/js-lol.yml | 2 +- .../config/locales/crowdin/js-lt.yml | 2 +- .../config/locales/crowdin/js-lv.yml | 2 +- .../config/locales/crowdin/js-ne.yml | 2 +- .../config/locales/crowdin/js-nl.yml | 2 +- .../config/locales/crowdin/js-no.yml | 2 +- .../config/locales/crowdin/js-pl.yml | 2 +- .../config/locales/crowdin/js-pt.yml | 2 +- .../config/locales/crowdin/js-ro.yml | 2 +- .../config/locales/crowdin/js-ru.yml | 2 +- .../config/locales/crowdin/js-rw.yml | 2 +- .../config/locales/crowdin/js-si.yml | 2 +- .../config/locales/crowdin/js-sk.yml | 2 +- .../config/locales/crowdin/js-sl.yml | 2 +- .../config/locales/crowdin/js-sv.yml | 2 +- .../config/locales/crowdin/js-th.yml | 2 +- .../config/locales/crowdin/js-tr.yml | 2 +- .../config/locales/crowdin/js-uk.yml | 2 +- .../config/locales/crowdin/js-vi.yml | 2 +- .../config/locales/crowdin/js-zh-TW.yml | 2 +- 90 files changed, 129 insertions(+), 129 deletions(-) diff --git a/config/locales/crowdin/js-af.yml b/config/locales/crowdin/js-af.yml index 01aa311563..918eb42d22 100644 --- a/config/locales/crowdin/js-af.yml +++ b/config/locales/crowdin/js-af.yml @@ -1165,7 +1165,7 @@ af: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Continue' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projekte' clear_selection: 'Clear selection' apply: 'Pas toe' diff --git a/config/locales/crowdin/js-ar.yml b/config/locales/crowdin/js-ar.yml index 7105e30e9e..600483d62a 100644 --- a/config/locales/crowdin/js-ar.yml +++ b/config/locales/crowdin/js-ar.yml @@ -1177,7 +1177,7 @@ ar: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Continue' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'المشاريع' clear_selection: 'Clear selection' apply: 'تطبيق' diff --git a/config/locales/crowdin/js-az.yml b/config/locales/crowdin/js-az.yml index 4bdff9d163..d725f070e0 100644 --- a/config/locales/crowdin/js-az.yml +++ b/config/locales/crowdin/js-az.yml @@ -1165,7 +1165,7 @@ az: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Continue' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projects' clear_selection: 'Clear selection' apply: 'Apply' diff --git a/config/locales/crowdin/js-bg.yml b/config/locales/crowdin/js-bg.yml index 7065958e23..f53fc7def2 100644 --- a/config/locales/crowdin/js-bg.yml +++ b/config/locales/crowdin/js-bg.yml @@ -1165,7 +1165,7 @@ bg: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Продължаване' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Проекти' clear_selection: 'Clear selection' apply: 'Приложи' diff --git a/config/locales/crowdin/js-ca.yml b/config/locales/crowdin/js-ca.yml index 4e6c5eed40..b91265e3ea 100644 --- a/config/locales/crowdin/js-ca.yml +++ b/config/locales/crowdin/js-ca.yml @@ -1165,7 +1165,7 @@ ca: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Continue' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projectes' clear_selection: 'Clear selection' apply: 'Aplicar' diff --git a/config/locales/crowdin/js-cs.yml b/config/locales/crowdin/js-cs.yml index 5abab78309..f3b7b8613f 100644 --- a/config/locales/crowdin/js-cs.yml +++ b/config/locales/crowdin/js-cs.yml @@ -1172,7 +1172,7 @@ cs: group: 'Skupina je nyní součástí %{project}. Mezitím již můžete s touto skupinou plánovat a přiřadit například pracovní balíčky.' next_button: 'Pokračovat' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projekty' clear_selection: 'Clear selection' apply: 'Použít' diff --git a/config/locales/crowdin/js-da.yml b/config/locales/crowdin/js-da.yml index 471ead041e..0eb0c3f68e 100644 --- a/config/locales/crowdin/js-da.yml +++ b/config/locales/crowdin/js-da.yml @@ -1164,7 +1164,7 @@ da: group: 'Gruppen er nu en del af %{project}. I mellemtiden kan du allerede planlægge med denne gruppe og tildele arbejdspakker for eksempel.' next_button: 'Fortsæt' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projekter' clear_selection: 'Clear selection' apply: 'Benyt' diff --git a/config/locales/crowdin/js-de.yml b/config/locales/crowdin/js-de.yml index 71cf2e5922..3549e091aa 100644 --- a/config/locales/crowdin/js-de.yml +++ b/config/locales/crowdin/js-de.yml @@ -1164,7 +1164,7 @@ de: group: 'Die Gruppe ist nun %{project} zugewiesen. In der Zwischenzeit können Sie bereits mit dieser Gruppe planen und ihr zum Beispiel Arbeitspakete zuweisen.' next_button: 'Fortfahren' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projekte' clear_selection: 'Clear selection' apply: 'Anwenden' diff --git a/config/locales/crowdin/js-el.yml b/config/locales/crowdin/js-el.yml index 8fb008e428..4e91613413 100644 --- a/config/locales/crowdin/js-el.yml +++ b/config/locales/crowdin/js-el.yml @@ -1164,7 +1164,7 @@ el: group: 'Η ομάδα είναι τώρα μέρος του %{project}. Εν τω μεταξύ, μπορείτε να προγραμματίσετε ήδη με αυτή την ομάδα και να αντιστοιχίσετε πακέτα εργασίας.' next_button: 'Συνέχεια' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Έργα' clear_selection: 'Clear selection' apply: 'Εφαρμογή' diff --git a/config/locales/crowdin/js-eo.yml b/config/locales/crowdin/js-eo.yml index 4eed1ce88f..dbde9783b0 100644 --- a/config/locales/crowdin/js-eo.yml +++ b/config/locales/crowdin/js-eo.yml @@ -1165,7 +1165,7 @@ eo: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Daŭrigi' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projektoj' clear_selection: 'Clear selection' apply: 'Ekuzi' diff --git a/config/locales/crowdin/js-es.yml b/config/locales/crowdin/js-es.yml index 5980ae4f14..3702aa5af4 100644 --- a/config/locales/crowdin/js-es.yml +++ b/config/locales/crowdin/js-es.yml @@ -1165,7 +1165,7 @@ es: group: 'El grupo ya forma parte de %{project}. Mientras tanto, puede crear planes con ese grupo y asignar paquetes de trabajo a este.' next_button: 'Continuar' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Proyectos' clear_selection: 'Clear selection' apply: 'Aplicar' diff --git a/config/locales/crowdin/js-et.yml b/config/locales/crowdin/js-et.yml index a0c6676925..a16df8e0fa 100644 --- a/config/locales/crowdin/js-et.yml +++ b/config/locales/crowdin/js-et.yml @@ -1165,7 +1165,7 @@ et: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Jätka' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projektid' clear_selection: 'Clear selection' apply: 'Rakenda' diff --git a/config/locales/crowdin/js-fa.yml b/config/locales/crowdin/js-fa.yml index 4662358c8f..756c9aae65 100644 --- a/config/locales/crowdin/js-fa.yml +++ b/config/locales/crowdin/js-fa.yml @@ -1165,7 +1165,7 @@ fa: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Continue' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projects' clear_selection: 'Clear selection' apply: 'درخواست' diff --git a/config/locales/crowdin/js-fi.yml b/config/locales/crowdin/js-fi.yml index d6217e097e..ff469613bc 100644 --- a/config/locales/crowdin/js-fi.yml +++ b/config/locales/crowdin/js-fi.yml @@ -1165,7 +1165,7 @@ fi: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Jatka' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projektit' clear_selection: 'Clear selection' apply: 'Ota käyttöön' diff --git a/config/locales/crowdin/js-fil.yml b/config/locales/crowdin/js-fil.yml index 0cd7811c7e..d1111d277b 100644 --- a/config/locales/crowdin/js-fil.yml +++ b/config/locales/crowdin/js-fil.yml @@ -1165,7 +1165,7 @@ fil: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Magpatuloy' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Mga proyekto' clear_selection: 'Clear selection' apply: 'Ilagay' diff --git a/config/locales/crowdin/js-fr.yml b/config/locales/crowdin/js-fr.yml index 962c5e47ea..a27404742e 100644 --- a/config/locales/crowdin/js-fr.yml +++ b/config/locales/crowdin/js-fr.yml @@ -1165,7 +1165,7 @@ fr: group: 'Le groupe fait maintenant partie du %{project}. Entre-temps, vous pouvez déjà planifier avec ce groupe et l''assigner des lots de travaux par exemple.' next_button: 'Continuer' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projets' clear_selection: 'Clear selection' apply: 'Appliquer' diff --git a/config/locales/crowdin/js-he.yml b/config/locales/crowdin/js-he.yml index 8676a2936a..7d0876e328 100644 --- a/config/locales/crowdin/js-he.yml +++ b/config/locales/crowdin/js-he.yml @@ -1171,7 +1171,7 @@ he: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Continue' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'פרויקטים' clear_selection: 'Clear selection' apply: 'החל' diff --git a/config/locales/crowdin/js-hi.yml b/config/locales/crowdin/js-hi.yml index 4aedc6d7fe..340e0c82ef 100644 --- a/config/locales/crowdin/js-hi.yml +++ b/config/locales/crowdin/js-hi.yml @@ -1165,7 +1165,7 @@ hi: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Continue' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projects' clear_selection: 'Clear selection' apply: 'लागू करें' diff --git a/config/locales/crowdin/js-hr.yml b/config/locales/crowdin/js-hr.yml index c1e8e91991..3955dbf658 100644 --- a/config/locales/crowdin/js-hr.yml +++ b/config/locales/crowdin/js-hr.yml @@ -1168,7 +1168,7 @@ hr: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Nastavi' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projekti' clear_selection: 'Clear selection' apply: 'Primjeni' diff --git a/config/locales/crowdin/js-hu.yml b/config/locales/crowdin/js-hu.yml index 9fb52fe8d9..4e9a5bee6f 100644 --- a/config/locales/crowdin/js-hu.yml +++ b/config/locales/crowdin/js-hu.yml @@ -1170,7 +1170,7 @@ hu: group: "A csoport mostantól a %{project} része. Eközben már tervezhet ezzel a csoporttal, és például munkacsomagokat rendelhet hozzá.\n" next_button: 'Folytatás' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projektek' clear_selection: 'Clear selection' apply: 'Alkalmaz' diff --git a/config/locales/crowdin/js-id.yml b/config/locales/crowdin/js-id.yml index 67ad351887..b56346a472 100644 --- a/config/locales/crowdin/js-id.yml +++ b/config/locales/crowdin/js-id.yml @@ -1162,7 +1162,7 @@ id: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Lanjut' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Project' clear_selection: 'Clear selection' apply: 'Apply' diff --git a/config/locales/crowdin/js-it.yml b/config/locales/crowdin/js-it.yml index df4ac2ab1c..a6c461e582 100644 --- a/config/locales/crowdin/js-it.yml +++ b/config/locales/crowdin/js-it.yml @@ -1166,7 +1166,7 @@ it: group: 'Il gruppo è ora parte di %{project}. Nel mentre puoi ad esempio già pianificare con quel gruppo e assegnare pacchetti di lavoro.' next_button: 'Continua' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Progetti' clear_selection: 'Clear selection' apply: 'Applica' diff --git a/config/locales/crowdin/js-ja.yml b/config/locales/crowdin/js-ja.yml index 8813a814c9..93c15ba7ff 100644 --- a/config/locales/crowdin/js-ja.yml +++ b/config/locales/crowdin/js-ja.yml @@ -1165,7 +1165,7 @@ ja: group: 'グループは %{project} の一部になりました。一方で、そのグループを使って計画を立てたり、ワークパッケージを割り当てたりすることができます。' next_button: '続行' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'プロジェクト' clear_selection: 'Clear selection' apply: '適用' diff --git a/config/locales/crowdin/js-ko.yml b/config/locales/crowdin/js-ko.yml index 5e0da95e81..eac70f0b41 100644 --- a/config/locales/crowdin/js-ko.yml +++ b/config/locales/crowdin/js-ko.yml @@ -1162,7 +1162,7 @@ ko: group: '이제 이 그룹은 %{project}의 일부입니다. 한편 당신은 해당 사용자에 대한 계획을 세우거나 작업 패키지를 할당할 수 있습니다.' next_button: '계속' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: '프로젝트' clear_selection: 'Clear selection' apply: '적용' diff --git a/config/locales/crowdin/js-lol.yml b/config/locales/crowdin/js-lol.yml index 8c4a0e09e9..a976f7a97c 100644 --- a/config/locales/crowdin/js-lol.yml +++ b/config/locales/crowdin/js-lol.yml @@ -1164,7 +1164,7 @@ lol: group: 'crwdns788732:0%{project}crwdne788732:0' next_button: 'crwdns788734:0crwdne788734:0' include_projects: - toggle_title: 'crwdns788736:0crwdne788736:0' + toggle_title: 'crwdns808124:0crwdne808124:0' title: 'crwdns788738:0crwdne788738:0' clear_selection: 'crwdns788740:0crwdne788740:0' apply: 'crwdns788742:0crwdne788742:0' diff --git a/config/locales/crowdin/js-lt.yml b/config/locales/crowdin/js-lt.yml index 161cbc11bd..3750c5218b 100644 --- a/config/locales/crowdin/js-lt.yml +++ b/config/locales/crowdin/js-lt.yml @@ -1171,7 +1171,7 @@ lt: group: 'Grupė jau tapo %{project} dalimi. Tuo pačiu, jūs jau galite planuoti darbus su šia grupe ir pavyzdžiui priskirti jai darbo užduotis.' next_button: 'Tęsti' include_projects: - toggle_title: 'Įtraukti projektus' + toggle_title: 'Include projects' title: 'Projektai' clear_selection: 'Valyti pasirinkimą' apply: 'Taikyti' diff --git a/config/locales/crowdin/js-lv.yml b/config/locales/crowdin/js-lv.yml index c4e23c47a3..c36670c793 100644 --- a/config/locales/crowdin/js-lv.yml +++ b/config/locales/crowdin/js-lv.yml @@ -1168,7 +1168,7 @@ lv: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Continue' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projekti' clear_selection: 'Clear selection' apply: 'Apstiprināt' diff --git a/config/locales/crowdin/js-ne.yml b/config/locales/crowdin/js-ne.yml index c2aace5433..a8a7a6f9db 100644 --- a/config/locales/crowdin/js-ne.yml +++ b/config/locales/crowdin/js-ne.yml @@ -1165,7 +1165,7 @@ ne: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Continue' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projects' clear_selection: 'Clear selection' apply: 'Apply' diff --git a/config/locales/crowdin/js-nl.yml b/config/locales/crowdin/js-nl.yml index 78d86775e8..7cc963ef82 100644 --- a/config/locales/crowdin/js-nl.yml +++ b/config/locales/crowdin/js-nl.yml @@ -1165,7 +1165,7 @@ nl: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Ga verder' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projecten' clear_selection: 'Clear selection' apply: 'Toepassen' diff --git a/config/locales/crowdin/js-no.yml b/config/locales/crowdin/js-no.yml index adbeef9d74..00fa169942 100644 --- a/config/locales/crowdin/js-no.yml +++ b/config/locales/crowdin/js-no.yml @@ -1165,7 +1165,7 @@ group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Fortsett' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Prosjekter' clear_selection: 'Clear selection' apply: 'Bruk' diff --git a/config/locales/crowdin/js-pl.yml b/config/locales/crowdin/js-pl.yml index 7e7b50b049..5758376e1e 100644 --- a/config/locales/crowdin/js-pl.yml +++ b/config/locales/crowdin/js-pl.yml @@ -1171,7 +1171,7 @@ pl: group: 'Grupa jest teraz częścią %{project}. Tymczasem możesz już planować z tą grupą i na przykład przypisać pakiety robocze.' next_button: 'Kontynuuj' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projekty' clear_selection: 'Clear selection' apply: 'Zastosuj' diff --git a/config/locales/crowdin/js-pt.yml b/config/locales/crowdin/js-pt.yml index abd80074bf..693cdf49ed 100644 --- a/config/locales/crowdin/js-pt.yml +++ b/config/locales/crowdin/js-pt.yml @@ -1164,7 +1164,7 @@ pt: group: 'O grupo agora faz parte de %{project}. Enquanto isso, você já pode planejar com este grupo e atribuir pacotes de trabalho por exemplo.' next_button: 'Continuar' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projetos' clear_selection: 'Clear selection' apply: 'Aplicar' diff --git a/config/locales/crowdin/js-ro.yml b/config/locales/crowdin/js-ro.yml index edcbba908b..ac3238a20f 100644 --- a/config/locales/crowdin/js-ro.yml +++ b/config/locales/crowdin/js-ro.yml @@ -1167,7 +1167,7 @@ ro: group: 'Grupul este acum o parte din %{project}. Între timp, puteți deja să planificați cu acest grup și să atribuiți pachete de lucru, de exemplu.' next_button: 'Continuaţi' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Proiecte' clear_selection: 'Clear selection' apply: 'Salvare' diff --git a/config/locales/crowdin/js-ru.yml b/config/locales/crowdin/js-ru.yml index e29b759c86..1124d18137 100644 --- a/config/locales/crowdin/js-ru.yml +++ b/config/locales/crowdin/js-ru.yml @@ -1170,7 +1170,7 @@ ru: group: 'Теперь группа является частью %{project}. Тем временем вы уже можете спланировать работу с этой группой и назначить пакеты работ.' next_button: 'Продолжить' include_projects: - toggle_title: 'Включить проекты' + toggle_title: 'Include projects' title: 'Проекты' clear_selection: 'Отменить выделение' apply: 'Применить' diff --git a/config/locales/crowdin/js-rw.yml b/config/locales/crowdin/js-rw.yml index 75e67a31a5..cbdd6f61d3 100644 --- a/config/locales/crowdin/js-rw.yml +++ b/config/locales/crowdin/js-rw.yml @@ -1165,7 +1165,7 @@ rw: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Continue' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projects' clear_selection: 'Clear selection' apply: 'Apply' diff --git a/config/locales/crowdin/js-si.yml b/config/locales/crowdin/js-si.yml index e327220db1..6f6132e067 100644 --- a/config/locales/crowdin/js-si.yml +++ b/config/locales/crowdin/js-si.yml @@ -1165,7 +1165,7 @@ si: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'ඉදිරියට යන්න' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'ව්‍යාපෘති' clear_selection: 'Clear selection' apply: 'අයදුම් කරන්න' diff --git a/config/locales/crowdin/js-sk.yml b/config/locales/crowdin/js-sk.yml index 74c262f336..45d2d5c6b8 100644 --- a/config/locales/crowdin/js-sk.yml +++ b/config/locales/crowdin/js-sk.yml @@ -1171,7 +1171,7 @@ sk: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Pokračovať' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projekty' clear_selection: 'Clear selection' apply: 'Použiť' diff --git a/config/locales/crowdin/js-sl.yml b/config/locales/crowdin/js-sl.yml index 8940e8a65d..4f660aea7d 100644 --- a/config/locales/crowdin/js-sl.yml +++ b/config/locales/crowdin/js-sl.yml @@ -1170,7 +1170,7 @@ sl: group: 'Skupina je zdaj del %{project}. Medtem lahko planirate s to skupino in ji dodelite delovne naloge.' next_button: 'Nadaljuj' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projekti' clear_selection: 'Clear selection' apply: 'Potrdi' diff --git a/config/locales/crowdin/js-sv.yml b/config/locales/crowdin/js-sv.yml index 1ce4e65a85..5abaed46dc 100644 --- a/config/locales/crowdin/js-sv.yml +++ b/config/locales/crowdin/js-sv.yml @@ -1164,7 +1164,7 @@ sv: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: 'Fortsätt' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projekt' clear_selection: 'Clear selection' apply: 'Tilldela' diff --git a/config/locales/crowdin/js-th.yml b/config/locales/crowdin/js-th.yml index 5c0dad2181..a3c68eef4f 100644 --- a/config/locales/crowdin/js-th.yml +++ b/config/locales/crowdin/js-th.yml @@ -1162,7 +1162,7 @@ th: group: 'ตอนนี้กลุ่มนี้เป็นส่วนหนึ่งของ %{project}, คุณสามารถวางแผนกับกลุ่มนั้นและกำหนดแพ็คเกจงานได้' next_button: 'Continue' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'โครงการ' clear_selection: 'Clear selection' apply: 'นำไปใช้' diff --git a/config/locales/crowdin/js-tr.yml b/config/locales/crowdin/js-tr.yml index 3116c0fbc8..1ceb792826 100644 --- a/config/locales/crowdin/js-tr.yml +++ b/config/locales/crowdin/js-tr.yml @@ -1165,7 +1165,7 @@ tr: group: 'Grup artık %{project} ''nin bir parçası. Bu arada, bu grupla zaten plan yapabilir ve örneğin iş paketleri atayabilirsiniz.' next_button: 'Devam et' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Projeler' clear_selection: 'Clear selection' apply: 'Uygula' diff --git a/config/locales/crowdin/js-uk.yml b/config/locales/crowdin/js-uk.yml index 1ca0c43a7c..0b48247918 100644 --- a/config/locales/crowdin/js-uk.yml +++ b/config/locales/crowdin/js-uk.yml @@ -1171,7 +1171,7 @@ uk: group: 'Ця група тепер входить у проєкт «%{project}». Тим часом ви вже можете включати її в план і призначати їй пакети робіт.' next_button: 'Продовжити' include_projects: - toggle_title: 'Включити проєкти' + toggle_title: 'Include projects' title: 'Проекти' clear_selection: 'Очистити вибране' apply: 'Застосувати' diff --git a/config/locales/crowdin/js-vi.yml b/config/locales/crowdin/js-vi.yml index 341b614170..4f2aaa2888 100644 --- a/config/locales/crowdin/js-vi.yml +++ b/config/locales/crowdin/js-vi.yml @@ -1161,7 +1161,7 @@ vi: group: 'Nhóm này hiện là một phần của %{project}. Trong khi đó, bạn có thể lên kế hoạch cho nhóm đó và chỉ định các gói làm việc cho từng trường hợp.' next_button: 'Tiếp tục' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: 'Các dự án' clear_selection: 'Clear selection' apply: 'Áp dụng' diff --git a/config/locales/crowdin/js-zh-TW.yml b/config/locales/crowdin/js-zh-TW.yml index 1d35a835dc..4b8f84b3aa 100644 --- a/config/locales/crowdin/js-zh-TW.yml +++ b/config/locales/crowdin/js-zh-TW.yml @@ -1161,7 +1161,7 @@ zh-TW: group: 'The group is now a part of %{project}. Meanwhile you can already plan with that group and assign work packages for instance.' next_button: '繼續' include_projects: - toggle_title: 'Include Projects' + toggle_title: 'Include projects' title: '專案' clear_selection: 'Clear selection' apply: '套用' diff --git a/config/locales/crowdin/lt.yml b/config/locales/crowdin/lt.yml index 84515182d1..177d78caa2 100644 --- a/config/locales/crowdin/lt.yml +++ b/config/locales/crowdin/lt.yml @@ -1260,7 +1260,7 @@ lt: error_enterprise_token_invalid_domain: "Enterprise Edition neaktyvi. Jūsų Enterprise rakto domenas (%{actual}) nesutampa su jūsų sistemos serverio vardu (%{expected})." error_failed_to_delete_entry: 'Šio įrašo nepavyko panaikinti.' error_in_dependent: "Klaida bandant pakeisti priklausomą objektą: %{dependent_class} #%{related_id} - %{related_subject}: %{error}" - error_in_new_dependent: "Error attempting to create dependent object: %{dependent_class} - %{related_subject}: %{error}" + error_in_new_dependent: "Klaida bandant sukurti priklausomą objektą: %{dependent_class} - %{related_subject}: %{error}" error_invalid_selected_value: "Netinkama parinkta reikšmė." error_journal_attribute_not_present: "Žurnale nėra atributo %{attribute}." error_pdf_export_too_many_columns: "PDF eksportavimui pasirinkta per daug stulpelių. Prašome sumažinti stulpelių skaičių." @@ -2915,7 +2915,7 @@ lt: work_package: "Jūsų ieškomas darbo paketas nerastas arba ištrintas." expected: date: "YYYY-MM-DD (ISO 8601 date only)" - datetime: "YYYY-MM-DDThh:mm:ss[.lll][+hh:mm] (any compatible ISO 8601 datetime)" + datetime: "YYYY-MM-DDThh:mm:ss[.lll][+hh:mm] (bet kokia ISO 8601 data/laikas)" duration: "ISO 8601 trukmė" invalid_content_type: "Laukiamas turinio tipas turi būti „%{content_type}“, bet gautas „%{actual}“." invalid_format: "Netinkamas formatas ypatybei '%{property}': tikėtasi panašaus į '%{expected_format}', bet gautas '%{actual}'." @@ -2927,7 +2927,7 @@ lt: select: "Prašomas %{invalid} pasirinkimas yra nepalaikomas. Palaikomi pasirinkimai yra %{supported}." invalid_user_status_transition: "Esama vartotojo paskyros būsena neleidžia šios operacijos." missing_content_type: "nenurodyta" - missing_property: "Missing property '%{property}'." + missing_property: "Trūkstama savybė '%{property}'." missing_request_body: "Nėra užklausos turinio." missing_or_malformed_parameter: "Užklausos '%{parameter}' parametras neegzistuoja arba yra neteisingai suformuluotas." multipart_body_error: "Užklausos turinys neturi laukiamų dalių." diff --git a/modules/storages/config/locales/crowdin/lt.yml b/modules/storages/config/locales/crowdin/lt.yml index 6b9ec84d89..2b6fe3c43b 100644 --- a/modules/storages/config/locales/crowdin/lt.yml +++ b/modules/storages/config/locales/crowdin/lt.yml @@ -1,65 +1,65 @@ #English strings go here lt: - permission_view_file_links: "View file links" - permission_manage_file_links: "Manage file links" - permission_manage_storages_in_project: "Manage file storages in project" - project_module_storages: "File storages" + permission_view_file_links: "Žiūrėti failo nuorodas" + permission_manage_file_links: "Tvarkyti failo nuorodas" + permission_manage_storages_in_project: "Tvarkyti failų saugyklas projekte" + project_module_storages: "Failų saugyklos" activerecord: models: - storages/storage: "Storage" + storages/storage: "Saugykla" attributes: storages/storage: - name: "Name" - creator: "Creator" - provider_type: "Provider type" + name: "Vardas" + creator: "Kūrėjas" + provider_type: "Tiekėjo tipas" storages/file_link: - origin_id: "Origin Id" + origin_id: "Kilmės Id" errors: messages: - not_linked_to_project: "is not linked to project." + not_linked_to_project: "nesusietas su projektu." models: storages/storage: attributes: host: - cannot_be_connected_to: "can not be connected to." - minimal_nextcloud_version_unmet: "does not meet minimal version requirements (must be Nextcloud 23 or higher)" - not_nextcloud_server: "is not a Nextcloud server" + cannot_be_connected_to: "negali būti prijungtas prie." + minimal_nextcloud_version_unmet: "neatitinka minimalių versijos reikalavimų (turi būti Nextcloud 23 ar aukštesnis)" + not_nextcloud_server: "nėra Nextcloud serveris" storages/file_link: attributes: origin_id: - only_numeric_or_uuid: "can only be numeric or uuid." + only_numeric_or_uuid: "gali būti tik skaitinis ar uuid." api_v3: errors: - too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." + too_many_elements_created_at_once: "Per daug elementu vienu metu. Tikėtasi daugiausia %{max}, gauta %{actual}." storages: page_titles: project_settings: - index: "File storages available in this project" - new: "Enable a file storage for this project" + index: "Šiame projekte prieinamos failų saugyklos." + new: "Įjungti failų saugyklą šiam projektui" instructions: - name: "Please use a short name. It will get used as a tab title in the work package view." - host: "The URL of your storage instance. The URL should not be more than 255 characters long." - no_storage_set_up: "There are no file storages set up, yet." - setting_up_additional_storages: "For setting up additional file storages, please visit" - setting_up_additional_storages_non_admin: "Administrators can set up additional file storages in Administration / File Storages." - setting_up_storages: "For setting up file storages, please visit" - setting_up_storages_non_admin: "Administrators can set up file storages in Administration / File Storages." - all_available_storages_already_added: "All available storages are already added to the project." + name: "Prašome naudoti trumpą pavadinimą. Jis bus naudojamas kaip kortelės antraštė darbų paketų vaizde." + host: "Jūsų saugyklos egzemplioriaus URL. URL negali būti ilgesnis už 255 simbolius." + no_storage_set_up: "Dar nėra nustatyta nei viena failų saugykla." + setting_up_additional_storages: "Norėdami sukurti papildomas failų saugyklas, prašome aplankyti" + setting_up_additional_storages_non_admin: "Administratoriai gali sukurti papildomas failų saugyklas: Administravimas / Failų saugyklos." + setting_up_storages: "Norėdami nustatyti failų saugyklas aplankykite" + setting_up_storages_non_admin: "Administratoriai gali nustatyti failų saugyklas: Administravimas / Failų saugyklos" + all_available_storages_already_added: "Visos galimos failų saugyklos jau pridėtos į projektą." delete_warning: storage: > - Are you sure you want to delete this storage? This will also delete the storage from all projects where it is used. Further, it will also delete all links from work packages to files that are stored in that storage. + Ar tikrai norite ištrinti šią failų saugyklą? Saugykla taipogi bus ištrinta iš visų projektų, kur ji naudojama. Taipogi tai ištrins visus ryšius iš darbo paketų į failus, esančius šioje saugykloje. project_storage: > - Are you sure you want to remove this file storage from this project? This will also remove all links to files stored in this storage for this project. - label_creator: "Creator" - label_file_link: "File link" - label_file_links: "File links" - label_name: "Name" - label_host: "Host" - label_provider_type: "Provider type" - label_new_storage: "New storage" - label_storage: "Storage" - label_storages: "Storages" - no_results: "No storages set up, yet." + Ar tikrai norite išimti šią failų saugyklą iš jūsų projekto? Taipogi bus išimtos visos šio projekto nuorodos į failus, saugomus šioje saugykloje. + label_creator: "Kūrėjas" + label_file_link: "Failo nuoroda" + label_file_links: "Failo nuorodos" + label_name: "Pavadinimas" + label_host: "Serveris" + label_provider_type: "Tiekėjo tipas" + label_new_storage: "Nauja saugykla" + label_storage: "Saugykla" + label_storages: "Saugyklos" + no_results: "Dar nenustatyta jokia saugykla" provider_types: - label: "Provider type" + label: "Tiekėjo tipas" nextcloud: "Nextcloud" diff --git a/modules/team_planner/config/locales/crowdin/js-af.yml b/modules/team_planner/config/locales/crowdin/js-af.yml index fb999cebd0..ab544bf686 100644 --- a/modules/team_planner/config/locales/crowdin/js-af.yml +++ b/modules/team_planner/config/locales/crowdin/js-af.yml @@ -2,7 +2,7 @@ af: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-ar.yml b/modules/team_planner/config/locales/crowdin/js-ar.yml index 46ccb4e50c..df3d9e3a07 100644 --- a/modules/team_planner/config/locales/crowdin/js-ar.yml +++ b/modules/team_planner/config/locales/crowdin/js-ar.yml @@ -2,7 +2,7 @@ ar: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-az.yml b/modules/team_planner/config/locales/crowdin/js-az.yml index ee42378b29..2b22e0cc84 100644 --- a/modules/team_planner/config/locales/crowdin/js-az.yml +++ b/modules/team_planner/config/locales/crowdin/js-az.yml @@ -2,7 +2,7 @@ az: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-bg.yml b/modules/team_planner/config/locales/crowdin/js-bg.yml index 8cff07556a..c6feb214db 100644 --- a/modules/team_planner/config/locales/crowdin/js-bg.yml +++ b/modules/team_planner/config/locales/crowdin/js-bg.yml @@ -2,7 +2,7 @@ bg: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-ca.yml b/modules/team_planner/config/locales/crowdin/js-ca.yml index 8104fa1a2c..c2f6c6eabd 100644 --- a/modules/team_planner/config/locales/crowdin/js-ca.yml +++ b/modules/team_planner/config/locales/crowdin/js-ca.yml @@ -2,7 +2,7 @@ ca: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-cs.yml b/modules/team_planner/config/locales/crowdin/js-cs.yml index 18da7da915..9d67b0280d 100644 --- a/modules/team_planner/config/locales/crowdin/js-cs.yml +++ b/modules/team_planner/config/locales/crowdin/js-cs.yml @@ -2,7 +2,7 @@ cs: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Týmový plánovač' unsaved_title: 'Nepojmenovaný plánovač týmu' diff --git a/modules/team_planner/config/locales/crowdin/js-da.yml b/modules/team_planner/config/locales/crowdin/js-da.yml index f37942c2ef..39582c27ce 100644 --- a/modules/team_planner/config/locales/crowdin/js-da.yml +++ b/modules/team_planner/config/locales/crowdin/js-da.yml @@ -2,7 +2,7 @@ da: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-de.yml b/modules/team_planner/config/locales/crowdin/js-de.yml index c1c253a175..fbb656c575 100644 --- a/modules/team_planner/config/locales/crowdin/js-de.yml +++ b/modules/team_planner/config/locales/crowdin/js-de.yml @@ -2,7 +2,7 @@ de: js: team_planner: - add_existing: 'Vorhandene hinzufügen' + add_existing: 'Add existing' create_new: 'Neuer Teamplaner' title: 'Teamplaner' unsaved_title: 'Unbenannter Teamplaner' diff --git a/modules/team_planner/config/locales/crowdin/js-el.yml b/modules/team_planner/config/locales/crowdin/js-el.yml index 05d8e48ea5..32c0b5c2bb 100644 --- a/modules/team_planner/config/locales/crowdin/js-el.yml +++ b/modules/team_planner/config/locales/crowdin/js-el.yml @@ -2,7 +2,7 @@ el: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-eo.yml b/modules/team_planner/config/locales/crowdin/js-eo.yml index b8fd831754..30451a6f20 100644 --- a/modules/team_planner/config/locales/crowdin/js-eo.yml +++ b/modules/team_planner/config/locales/crowdin/js-eo.yml @@ -2,7 +2,7 @@ eo: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-es.yml b/modules/team_planner/config/locales/crowdin/js-es.yml index 0c1d2ff693..a5003fbbfb 100644 --- a/modules/team_planner/config/locales/crowdin/js-es.yml +++ b/modules/team_planner/config/locales/crowdin/js-es.yml @@ -2,7 +2,7 @@ es: js: team_planner: - add_existing: 'Añadir existente' + add_existing: 'Add existing' create_new: 'Nuevo planificador de equipo' title: 'Planificador de equipos' unsaved_title: 'Planificador de equipo anonimo' diff --git a/modules/team_planner/config/locales/crowdin/js-et.yml b/modules/team_planner/config/locales/crowdin/js-et.yml index c2b736bbd8..0ab82c7c78 100644 --- a/modules/team_planner/config/locales/crowdin/js-et.yml +++ b/modules/team_planner/config/locales/crowdin/js-et.yml @@ -2,7 +2,7 @@ et: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-fa.yml b/modules/team_planner/config/locales/crowdin/js-fa.yml index 92d1540f5c..3ed68526a2 100644 --- a/modules/team_planner/config/locales/crowdin/js-fa.yml +++ b/modules/team_planner/config/locales/crowdin/js-fa.yml @@ -2,7 +2,7 @@ fa: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-fi.yml b/modules/team_planner/config/locales/crowdin/js-fi.yml index f187b34419..e512c4c427 100644 --- a/modules/team_planner/config/locales/crowdin/js-fi.yml +++ b/modules/team_planner/config/locales/crowdin/js-fi.yml @@ -2,7 +2,7 @@ fi: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-fil.yml b/modules/team_planner/config/locales/crowdin/js-fil.yml index e584c7eaf5..dd77e899ea 100644 --- a/modules/team_planner/config/locales/crowdin/js-fil.yml +++ b/modules/team_planner/config/locales/crowdin/js-fil.yml @@ -2,7 +2,7 @@ fil: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-fr.yml b/modules/team_planner/config/locales/crowdin/js-fr.yml index 907bbf696c..f0e5ce4f3f 100644 --- a/modules/team_planner/config/locales/crowdin/js-fr.yml +++ b/modules/team_planner/config/locales/crowdin/js-fr.yml @@ -2,7 +2,7 @@ fr: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Planificateur d''équipe' unsaved_title: 'Planificateur d''équipe sans nom' diff --git a/modules/team_planner/config/locales/crowdin/js-he.yml b/modules/team_planner/config/locales/crowdin/js-he.yml index e9fea6753c..46e83c277e 100644 --- a/modules/team_planner/config/locales/crowdin/js-he.yml +++ b/modules/team_planner/config/locales/crowdin/js-he.yml @@ -2,7 +2,7 @@ he: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-hi.yml b/modules/team_planner/config/locales/crowdin/js-hi.yml index c914eb7093..7493cf9c1c 100644 --- a/modules/team_planner/config/locales/crowdin/js-hi.yml +++ b/modules/team_planner/config/locales/crowdin/js-hi.yml @@ -2,7 +2,7 @@ hi: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-hr.yml b/modules/team_planner/config/locales/crowdin/js-hr.yml index 39ec648e6e..9e8e377fcd 100644 --- a/modules/team_planner/config/locales/crowdin/js-hr.yml +++ b/modules/team_planner/config/locales/crowdin/js-hr.yml @@ -2,7 +2,7 @@ hr: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-hu.yml b/modules/team_planner/config/locales/crowdin/js-hu.yml index 1ba710069b..824bb51553 100644 --- a/modules/team_planner/config/locales/crowdin/js-hu.yml +++ b/modules/team_planner/config/locales/crowdin/js-hu.yml @@ -2,7 +2,7 @@ hu: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-id.yml b/modules/team_planner/config/locales/crowdin/js-id.yml index 32ebcc15df..12cc0ddc04 100644 --- a/modules/team_planner/config/locales/crowdin/js-id.yml +++ b/modules/team_planner/config/locales/crowdin/js-id.yml @@ -2,7 +2,7 @@ id: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-it.yml b/modules/team_planner/config/locales/crowdin/js-it.yml index 544c1eccdd..2edbc6eb5c 100644 --- a/modules/team_planner/config/locales/crowdin/js-it.yml +++ b/modules/team_planner/config/locales/crowdin/js-it.yml @@ -2,7 +2,7 @@ it: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-ja.yml b/modules/team_planner/config/locales/crowdin/js-ja.yml index 0a7c44e34b..c9a653ca8f 100644 --- a/modules/team_planner/config/locales/crowdin/js-ja.yml +++ b/modules/team_planner/config/locales/crowdin/js-ja.yml @@ -2,7 +2,7 @@ ja: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-ko.yml b/modules/team_planner/config/locales/crowdin/js-ko.yml index 351d7899af..a3399298e6 100644 --- a/modules/team_planner/config/locales/crowdin/js-ko.yml +++ b/modules/team_planner/config/locales/crowdin/js-ko.yml @@ -2,7 +2,7 @@ ko: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-lol.yml b/modules/team_planner/config/locales/crowdin/js-lol.yml index 017d4ec63c..e784ef800e 100644 --- a/modules/team_planner/config/locales/crowdin/js-lol.yml +++ b/modules/team_planner/config/locales/crowdin/js-lol.yml @@ -2,7 +2,7 @@ lol: js: team_planner: - add_existing: 'crwdns750836:0crwdne750836:0' + add_existing: 'crwdns808126:0crwdne808126:0' create_new: 'crwdns773050:0crwdne773050:0' title: 'crwdns500087:0crwdne500087:0' unsaved_title: 'crwdns500089:0crwdne500089:0' diff --git a/modules/team_planner/config/locales/crowdin/js-lt.yml b/modules/team_planner/config/locales/crowdin/js-lt.yml index bb02b071a8..f1157d84b4 100644 --- a/modules/team_planner/config/locales/crowdin/js-lt.yml +++ b/modules/team_planner/config/locales/crowdin/js-lt.yml @@ -2,7 +2,7 @@ lt: js: team_planner: - add_existing: 'Pridėti esamą' + add_existing: 'Add existing' create_new: 'Naujas komandos planas' title: 'Komandos planas' unsaved_title: 'Nepavadintas komandos planas' diff --git a/modules/team_planner/config/locales/crowdin/js-lv.yml b/modules/team_planner/config/locales/crowdin/js-lv.yml index 52a951d0af..bb4b839f0d 100644 --- a/modules/team_planner/config/locales/crowdin/js-lv.yml +++ b/modules/team_planner/config/locales/crowdin/js-lv.yml @@ -2,7 +2,7 @@ lv: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-ne.yml b/modules/team_planner/config/locales/crowdin/js-ne.yml index 9f24cc26af..97279dd818 100644 --- a/modules/team_planner/config/locales/crowdin/js-ne.yml +++ b/modules/team_planner/config/locales/crowdin/js-ne.yml @@ -2,7 +2,7 @@ ne: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-nl.yml b/modules/team_planner/config/locales/crowdin/js-nl.yml index 49e4981851..b8c7cde6d8 100644 --- a/modules/team_planner/config/locales/crowdin/js-nl.yml +++ b/modules/team_planner/config/locales/crowdin/js-nl.yml @@ -2,7 +2,7 @@ nl: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-no.yml b/modules/team_planner/config/locales/crowdin/js-no.yml index 0468b299db..bc8d01bcd8 100644 --- a/modules/team_planner/config/locales/crowdin/js-no.yml +++ b/modules/team_planner/config/locales/crowdin/js-no.yml @@ -2,7 +2,7 @@ "no": js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-pl.yml b/modules/team_planner/config/locales/crowdin/js-pl.yml index abcec089a8..f8e516067e 100644 --- a/modules/team_planner/config/locales/crowdin/js-pl.yml +++ b/modules/team_planner/config/locales/crowdin/js-pl.yml @@ -2,7 +2,7 @@ pl: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-pt.yml b/modules/team_planner/config/locales/crowdin/js-pt.yml index 857ea1fa3d..989b1e62bb 100644 --- a/modules/team_planner/config/locales/crowdin/js-pt.yml +++ b/modules/team_planner/config/locales/crowdin/js-pt.yml @@ -2,7 +2,7 @@ pt: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Planejador de equipe' unsaved_title: 'Planejador de equipe não nomeado' diff --git a/modules/team_planner/config/locales/crowdin/js-ro.yml b/modules/team_planner/config/locales/crowdin/js-ro.yml index 080c582046..b69588d5a0 100644 --- a/modules/team_planner/config/locales/crowdin/js-ro.yml +++ b/modules/team_planner/config/locales/crowdin/js-ro.yml @@ -2,7 +2,7 @@ ro: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Planificator de echipă' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-ru.yml b/modules/team_planner/config/locales/crowdin/js-ru.yml index a5e5daf038..869ebcd274 100644 --- a/modules/team_planner/config/locales/crowdin/js-ru.yml +++ b/modules/team_planner/config/locales/crowdin/js-ru.yml @@ -2,7 +2,7 @@ ru: js: team_planner: - add_existing: 'Добавить существующее' + add_existing: 'Add existing' create_new: 'Новый планировщик для команды' title: 'Командный планировщик' unsaved_title: 'Безымянный планировщик команды' diff --git a/modules/team_planner/config/locales/crowdin/js-rw.yml b/modules/team_planner/config/locales/crowdin/js-rw.yml index 2e4893afc4..1933116f17 100644 --- a/modules/team_planner/config/locales/crowdin/js-rw.yml +++ b/modules/team_planner/config/locales/crowdin/js-rw.yml @@ -2,7 +2,7 @@ rw: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-si.yml b/modules/team_planner/config/locales/crowdin/js-si.yml index 8b4a1cb905..ed8173195a 100644 --- a/modules/team_planner/config/locales/crowdin/js-si.yml +++ b/modules/team_planner/config/locales/crowdin/js-si.yml @@ -2,7 +2,7 @@ si: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-sk.yml b/modules/team_planner/config/locales/crowdin/js-sk.yml index c25ebbb362..5d9ff4a8bc 100644 --- a/modules/team_planner/config/locales/crowdin/js-sk.yml +++ b/modules/team_planner/config/locales/crowdin/js-sk.yml @@ -2,7 +2,7 @@ sk: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-sl.yml b/modules/team_planner/config/locales/crowdin/js-sl.yml index b6d0e3d60b..b4ca49292b 100644 --- a/modules/team_planner/config/locales/crowdin/js-sl.yml +++ b/modules/team_planner/config/locales/crowdin/js-sl.yml @@ -2,7 +2,7 @@ sl: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-sv.yml b/modules/team_planner/config/locales/crowdin/js-sv.yml index 9f4920802b..83d769cc09 100644 --- a/modules/team_planner/config/locales/crowdin/js-sv.yml +++ b/modules/team_planner/config/locales/crowdin/js-sv.yml @@ -2,7 +2,7 @@ sv: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-th.yml b/modules/team_planner/config/locales/crowdin/js-th.yml index c75510abe9..057b4ae151 100644 --- a/modules/team_planner/config/locales/crowdin/js-th.yml +++ b/modules/team_planner/config/locales/crowdin/js-th.yml @@ -2,7 +2,7 @@ th: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-tr.yml b/modules/team_planner/config/locales/crowdin/js-tr.yml index 3617389e78..a05ea00b9d 100644 --- a/modules/team_planner/config/locales/crowdin/js-tr.yml +++ b/modules/team_planner/config/locales/crowdin/js-tr.yml @@ -2,7 +2,7 @@ tr: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-uk.yml b/modules/team_planner/config/locales/crowdin/js-uk.yml index 33010eb1a2..f281a14e72 100644 --- a/modules/team_planner/config/locales/crowdin/js-uk.yml +++ b/modules/team_planner/config/locales/crowdin/js-uk.yml @@ -2,7 +2,7 @@ uk: js: team_planner: - add_existing: 'Додати наявний' + add_existing: 'Add existing' create_new: 'Новий командний планувальник' title: 'Командний планувальник' unsaved_title: 'Командний планувальник без назви' diff --git a/modules/team_planner/config/locales/crowdin/js-vi.yml b/modules/team_planner/config/locales/crowdin/js-vi.yml index a31cf8a535..c65a63fa57 100644 --- a/modules/team_planner/config/locales/crowdin/js-vi.yml +++ b/modules/team_planner/config/locales/crowdin/js-vi.yml @@ -2,7 +2,7 @@ vi: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' diff --git a/modules/team_planner/config/locales/crowdin/js-zh-TW.yml b/modules/team_planner/config/locales/crowdin/js-zh-TW.yml index a455b470cb..57526576aa 100644 --- a/modules/team_planner/config/locales/crowdin/js-zh-TW.yml +++ b/modules/team_planner/config/locales/crowdin/js-zh-TW.yml @@ -2,7 +2,7 @@ zh-TW: js: team_planner: - add_existing: 'Add Existing' + add_existing: 'Add existing' create_new: 'New team planner' title: 'Team planner' unsaved_title: 'Unnamed team planner' From 2ea012d0e2defff7dfd99ea87d3edc0824c66752 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Mar 2022 22:20:58 +0000 Subject: [PATCH 10/60] Bump puma from 5.6.2 to 5.6.4 Bumps [puma](https://github.com/puma/puma) from 5.6.2 to 5.6.4. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/master/History.md) - [Commits](https://github.com/puma/puma/compare/v5.6.2...v5.6.4) --- updated-dependencies: - dependency-name: puma dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index b697ed72ae..807c1c64e4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -707,7 +707,7 @@ GEM eventmachine_httpserver http_parser.rb (~> 0.6.0) multi_json - puma (5.6.2) + puma (5.6.4) nio4r (~> 2.0) puma-plugin-statsd (2.1.0) puma (>= 5.0, < 6) From 22cf9ea47cfd8caf3fa0990f7f2d637a18873fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 30 Mar 2022 10:48:20 +0200 Subject: [PATCH 11/60] Add LDAP user synchronization job --- app/models/auth_source.rb | 2 + app/models/ldap_auth_source.rb | 8 +- .../ldap/synchronize_users_service.rb | 95 +++++++++++++++ app/workers/ldap/synchronization_job.rb | 49 ++++++++ config/initializers/cronjobs.rb | 3 +- lib/open_project/mutex.rb | 13 +- lib/tasks/ldap.rake | 87 +------------ .../spec/services/synchronization_spec.rb | 12 +- ...chronize_users_service_integration_spec.rb | 115 ++++++++++++++++++ spec/support/shared/with_test_ldap.rb | 64 ++++++++++ 10 files changed, 346 insertions(+), 102 deletions(-) create mode 100644 app/services/ldap/synchronize_users_service.rb create mode 100644 app/workers/ldap/synchronization_job.rb create mode 100644 spec/services/ldap/synchronize_users_service_integration_spec.rb create mode 100644 spec/support/shared/with_test_ldap.rb diff --git a/app/models/auth_source.rb b/app/models/auth_source.rb index 7b9d176f9f..d678a3b11f 100644 --- a/app/models/auth_source.rb +++ b/app/models/auth_source.rb @@ -29,6 +29,8 @@ class AuthSource < ApplicationRecord include Redmine::Ciphering + class Error < ::StandardError; end + has_many :users validates :name, diff --git a/app/models/ldap_auth_source.rb b/app/models/ldap_auth_source.rb index 345c551719..545e0d23a5 100644 --- a/app/models/ldap_auth_source.rb +++ b/app/models/ldap_auth_source.rb @@ -53,7 +53,7 @@ class LdapAuthSource < AuthSource attrs.except(:dn) end rescue Net::LDAP::Error => e - raise 'LdapError: ' + e.message + raise AuthSource::Error, "LdapError: #{e.message}" end def find_user(login) @@ -66,7 +66,7 @@ class LdapAuthSource < AuthSource attrs.except(:dn) end rescue Net::LDAP::Error => e - raise 'LdapError: ' + e.message + raise AuthSource::Error, "LdapError: #{e.message}" end # Open and return a system connection @@ -77,10 +77,10 @@ class LdapAuthSource < AuthSource # test the connection to the LDAP def test_connection unless authenticate_dn(account, account_password) - raise I18n.t('auth_source.ldap_error', error_message: I18n.t('auth_source.ldap_auth_failed')) + raise AuthSource::Error, I18n.t('auth_source.ldap_error', error_message: I18n.t('auth_source.ldap_auth_failed')) end rescue Net::LDAP::Error => e - raise I18n.t('auth_source.ldap_error', error_message: e.to_s) + raise AuthSource::Error, I18n.t('auth_source.ldap_error', error_message: e.to_s) end def auth_method_name diff --git a/app/services/ldap/synchronize_users_service.rb b/app/services/ldap/synchronize_users_service.rb new file mode 100644 index 0000000000..401dd6768a --- /dev/null +++ b/app/services/ldap/synchronize_users_service.rb @@ -0,0 +1,95 @@ +module Ldap + class SynchronizeUsersService + attr_reader :ldap, :logins + + def initialize(ldap, logins = nil) + @ldap = ldap + @logins = logins + end + + def call + Rails.logger.debug { "Start LDAP user synchronization for #{ldap.name}." } + User.system.run_given do + OpenProject::Mutex.with_advisory_lock_transaction(ldap, 'synchronize_users') do + synchronize! + end + end + end + + private + + def synchronize! + ldap_con = new_ldap_connection + + applicable_users.find_each do |user| + synchronize_user(user, ldap_con) + rescue ::AuthSource::Error => e + Rails.logger.error { "Failed to synchronize user #{ldap.name} due to LDAP error: #{e.message}" } + # Reset the LDAP connection + ldap_con = new_ldap_connection + rescue StandardError => e + Rails.logger.error { "Failed to synchronize user #{ldap.name}: #{e.message}" } + end + end + + # Get the applicable users + # as the service can be called with just a subset of users + # from rake/external services. + def applicable_users + if logins.present? + ldap.users.where("LOWER(login) in (?)", logins.map(&:downcase)) + else + ldap.users + end + end + + def new_ldap_connection + ldap.instance_eval { initialize_ldap_con(account, account_password) } + end + + def synchronize_user(user, ldap_con) + Rails.logger.debug { "Synchronizing user #{user.login}." } + + update_attributes = user_attributes(user.login, ldap_con) + if update_attributes + try_to_update(user, update_attributes) + else + Rails.logger.info { "Could not find user #{user.login} in #{ldap.name}. Locking the user." } + user.update_column(:status, Principal.statuses[:locked]) + end + end + + # Try to create the user from attributes + # rubocop:disable Metrics/AbcSize + def try_to_update(user, attrs) + call = Users::UpdateService + .new(model: user, user: User.system) + .call(attrs.merge) + + if call.success? + # Ensure the user is activated + call.result.update_column(:status, Principal.statuses[:active]) + Rails.logger.info { "[LDAP user sync] User '#{call.result.login}' updated" } + else + Rails.logger.error { "[LDAP user sync] User '#{user.login}' could not be updated: #{call.message}" } + end + end + # rubocop:enable Metrics/AbcSize + + def user_attributes(login, ldap_con) + # Get user login attribute and base dn which are private + base_dn = ldap.base_dn + + search_attributes = ldap.search_attributes(true) + login_filter = Net::LDAP::Filter.eq(ldap.attr_login, login) + ldap_con.search(base: base_dn, + filter: ldap.default_filter & login_filter, + attributes: search_attributes) do |entry| + data = ldap.get_user_attributes_from_ldap_entry(entry) + return data.except(:dn) + end + + nil + end + end +end diff --git a/app/workers/ldap/synchronization_job.rb b/app/workers/ldap/synchronization_job.rb new file mode 100644 index 0000000000..776967bf53 --- /dev/null +++ b/app/workers/ldap/synchronization_job.rb @@ -0,0 +1,49 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2022 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Ldap + class SynchronizationJob < ::Cron::CronJob + # Run once per night at 11:30pm + self.cron_expression = '30 23 * * *' + + def perform + run_user_sync + end + + private + + def run_user_sync + return if OpenProject::Configuration.ldap_users_disable_sync_job? + + ::LdapAuthSource.find_each do |ldap| + Rails.logger.info { "[LDAP groups] Synchronizing users for LDAP connection #{ldap.name}" } + ::Ldap::SynchronizeUsersService.new(ldap).perform + end + end + end +end diff --git a/config/initializers/cronjobs.rb b/config/initializers/cronjobs.rb index e41da17a05..3273c00a18 100644 --- a/config/initializers/cronjobs.rb +++ b/config/initializers/cronjobs.rb @@ -7,6 +7,7 @@ OpenProject::Application.configure do |application| ::Cron::ClearUploadedFilesJob, ::OAuth::CleanupJob, ::Attachments::CleanupUncontaineredJob, - ::Notifications::ScheduleReminderMailsJob + ::Notifications::ScheduleReminderMailsJob, + ::Ldap::SynchronizationJob end end diff --git a/lib/open_project/mutex.rb b/lib/open_project/mutex.rb index 1dd1b34bf6..84d172905c 100644 --- a/lib/open_project/mutex.rb +++ b/lib/open_project/mutex.rb @@ -54,17 +54,18 @@ module OpenProject # attachable journals for the attachment added by themselves. To the user this will look as if one of the actions # deleted the other attachment. The next action, Action 3, will then seem to have readded the attachment, # seemingly removed before. - def with_advisory_lock_transaction(entry, &block) + def with_advisory_lock_transaction(entry, suffix = nil, &block) + lock_name = "mutex_on_#{entry.class.name}_#{entry.id}" + lock_name << "_#{suffix}" if suffix + ActiveRecord::Base.transaction do - with_advisory_lock(entry, &block) + with_advisory_lock(entry.class, lock_name, &block) end end - def with_advisory_lock(entry) - lock_name = "mutex_on_#{entry.class.name}_#{entry.id}" - + def with_advisory_lock(resource_class, lock_name) debug_log("Attempting to fetched advisory lock", lock_name) - result = entry.class.with_advisory_lock(lock_name, transaction: true) do + result = resource_class.with_advisory_lock(lock_name, transaction: true) do debug_log("Fetched advisory lock", lock_name) yield end diff --git a/lib/tasks/ldap.rake b/lib/tasks/ldap.rake index 48271a5443..ef372b9888 100644 --- a/lib/tasks/ldap.rake +++ b/lib/tasks/ldap.rake @@ -38,91 +38,16 @@ namespace :ldap do args end - desc 'Synchronize users from the LDAP auth source with an optional filter' \ - 'rake ldap:sync["name=", filter=]' + desc 'Synchronize existing users from the LDAP auth source' \ + 'rake ldap:sync["name=", users=]' task sync: :environment do args = parse_args ldap = LdapAuthSource.find_by!(name: args.fetch(:name)) - # Parse filter string if available - filter = Net::LDAP::Filter.from_rfc2254 args.fetch(:filter, 'objectClass = *') - - # Open LDAP connection - ldap_con = ldap.send(:initialize_ldap_con, ldap.account, ldap.account_password) - - User.transaction do - ldap_con.search(base: ldap.base_dn, filter: filter) do |entry| - user = User.find_or_initialize_by(login: entry[ldap.attr_login]) - user.attributes = { - firstname: entry[ldap.attr_firstname], - lastname: entry[ldap.attr_lastname], - mail: entry[ldap.attr_mail], - admin: entry[ldap.attr_admin], - auth_source: ldap - } - - if user.changed? - prefix = user.new_record? ? 'Created' : 'Updated' - Rails.logger.info "#{prefix} user #{user.login} from ldap synchronization" - user.save - end - end - end - end - - desc 'Synchronize a list of user IDs with the LDAP auth source' \ - 'rake ldap:sync_user_list LDAP=" USERLIST=' - task sync_user_list: :environment do - file = ENV['USERLIST'] - ldap_name = ENV['LDAP'] - - ldap = LdapAuthSource.find_by!(name: ldap_name) - - # Open connection - puts "--> Opening LDAP connection" - ldap_con = ldap.instance_eval { initialize_ldap_con(account, account_password) } - base_dn = ldap.send :base_dn - - attr_login = ldap.send :attr_login - object_filter = Net::LDAP::Filter.eq('objectClass', '*') - attributes = ldap.instance_eval do - ['dn', attr_login, attr_firstname, attr_lastname, attr_mail, attr_admin] - end - - puts "--> Reading username file #{file}" - File.read(file).lines.each do |username| - # Trim and skip empty - username.chomp! - next unless username.present? - - login_filter = Net::LDAP::Filter.eq(attr_login, username) - - entries = ldap_con.search(base: base_dn, filter: object_filter & login_filter, attributes: attributes) - - if entries.count == 0 - warn "Did not find entry for #{username}" - next - end - - if entries.count > 1 - warn "Found multiple entries for #{username}: #{entries.map(&:dn)}. Skipping" - next - end - - entry = entries.first - user = User.find_or_initialize_by(login: LdapAuthSource.get_attr(entry, ldap.attr_login)) - user.attributes = ldap.send(:get_user_attributes_from_ldap_entry, entry).except(:dn) - - if user.changed? - prefix = user.new_record? ? 'Created' : 'Updated' - - if user.save - puts "#{prefix} user #{user.login} from ldap synchronization" - else - puts "Failed to save #{user.login}: #{user.errors.full_messages.join(', ')}." - end - end - end + logins = args.fetch(:logins, '').split(/\s*,\s*/) + ::Ldap::SynchronizeUsersService + .new(ldap, logins) + .call end desc 'Register a LDAP auth source for the given LDAP URL and attribute mapping: ' \ diff --git a/modules/ldap_groups/spec/services/synchronization_spec.rb b/modules/ldap_groups/spec/services/synchronization_spec.rb index 72d5bfee49..295accf235 100644 --- a/modules/ldap_groups/spec/services/synchronization_spec.rb +++ b/modules/ldap_groups/spec/services/synchronization_spec.rb @@ -2,20 +2,12 @@ require File.dirname(__FILE__) + '/../spec_helper' require 'ladle' describe LdapGroups::SynchronizeGroupsService, with_ee: %i[ldap_groups] do + include_context 'with temporary LDAP' + let(:plugin_settings) do { group_base: 'ou=groups,dc=example,dc=com', group_key: 'cn' } end - before(:all) do - ldif = Rails.root.join('spec/fixtures/ldap/users.ldif') - @ldap_server = Ladle::Server.new(quiet: false, port: ParallelHelper.port_for_ldap.to_s, domain: 'dc=example,dc=com', - ldif: ldif).start - end - - after(:all) do - @ldap_server.stop - end - # Ldap has: # three users aa729, bb459, cc414 # two groups foo (aa729), bar(aa729, bb459, cc414) diff --git a/spec/services/ldap/synchronize_users_service_integration_spec.rb b/spec/services/ldap/synchronize_users_service_integration_spec.rb new file mode 100644 index 0000000000..52e9b5f24e --- /dev/null +++ b/spec/services/ldap/synchronize_users_service_integration_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +describe Ldap::SynchronizeUsersService do + include_context 'with temporary LDAP' + + subject do + described_class.new(auth_source).call + end + + context 'when updating users' do + let!(:user_aa729) { create :user, login: 'aa729', firstname: 'Foobar', auth_source: auth_source } + let!(:user_bb459) { create :user, login: 'bb459', firstname: 'Bla', auth_source: auth_source } + + it 'updates the attributes of those users' do + subject + + user_aa729.reload + user_bb459.reload + + expect(user_aa729.firstname).to eq 'Alexandra' + expect(user_aa729.lastname).to eq 'Adams' + expect(user_aa729.mail).to eq 'alexandra@example.org' + + expect(user_bb459.firstname).to eq 'Belle' + expect(user_bb459.lastname).to eq 'Baldwin' + expect(user_bb459.mail).to eq 'belle@example.org' + end + + it 'updates one user if the other fails' do + allow(Users::UpdateService) + .to receive(:new) + .and_call_original + + allow(Users::UpdateService) + .to receive(:new) + .with(model: user_aa729, user: User.system) + .and_raise("Some bad error happening here") + + subject + + user_aa729.reload + user_bb459.reload + + expect(user_aa729.firstname).to eq 'Foobar' + expect(user_aa729.lastname).to eq 'Bobbit' + + expect(user_bb459.firstname).to eq 'Belle' + expect(user_bb459.lastname).to eq 'Baldwin' + expect(user_bb459.mail).to eq 'belle@example.org' + end + + it 'reactivates the account if it is locked' do + user_aa729.lock! + + expect(user_aa729.reload).to be_locked + + subject + + expect(user_aa729.reload).not_to be_locked + expect(user_aa729).to be_active + end + + context 'when requesting only a subset of users' do + let!(:user_cc414) { create :user, login: 'cc414', auth_source: auth_source } + + subject do + described_class.new(auth_source, %w[Aa729 cc414]).call + end + + it 'syncs all case-insensitive users' do + subject + + user_aa729.reload + user_bb459.reload + user_cc414.reload + + expect(user_aa729.firstname).to eq 'Alexandra' + expect(user_aa729.lastname).to eq 'Adams' + expect(user_aa729.mail).to eq 'alexandra@example.org' + + expect(user_cc414.firstname).to eq 'Claire' + expect(user_cc414.lastname).to eq 'Carpenter' + expect(user_cc414.mail).to eq 'claire@example.org' + + expect(user_bb459.firstname).to eq 'Bla' + expect(user_bb459.lastname).to eq 'Bobbit' + end + end + end + + context 'with a user that is no longer in LDAP' do + let(:user_foo) { create :user, login: 'login', auth_source: auth_source } + + it 'locks that user' do + expect(user_foo).to be_active + + subject + + expect(user_foo.reload).to be_locked + end + end + + context 'with a user that is in another LDAP' do + let(:auth_source2) { create :ldap_auth_source, name: 'Another LDAP' } + let(:user_foo) { create :user, login: 'login', auth_source: auth_source2 } + + it 'does not touch that user' do + expect(user_foo).to be_active + + subject + + expect(user_foo.reload).to be_active + end + end +end diff --git a/spec/support/shared/with_test_ldap.rb b/spec/support/shared/with_test_ldap.rb new file mode 100644 index 0000000000..f564ed402e --- /dev/null +++ b/spec/support/shared/with_test_ldap.rb @@ -0,0 +1,64 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2022 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ +require 'ladle' + +shared_context 'with temporary LDAP' do + # rubocop:disable RSpec/InstanceVariable + before(:all) do + ldif = Rails.root.join('spec/fixtures/ldap/users.ldif') + @ldap_server = Ladle::Server.new(quiet: false, + port: ParallelHelper.port_for_ldap.to_s, + domain: 'dc=example,dc=com', + ldif: ldif).start + end + + after(:all) do + @ldap_server.stop + end + # rubocop:enable RSpec/InstanceVariable + + # Ldap has: + # three users aa729, bb459, cc414 + # two groups foo (aa729), bar(aa729, bb459, cc414) + let(:auth_source) do + create :ldap_auth_source, + port: ParallelHelper.port_for_ldap.to_s, + account: 'uid=admin,ou=system', + account_password: 'secret', + base_dn: 'ou=people,dc=example,dc=com', + onthefly_register: onthefly_register, + filter_string: ldap_filter, + attr_login: 'uid', + attr_firstname: 'givenName', + attr_lastname: 'sn', + attr_mail: 'mail' + end + + let(:onthefly_register) { false } + let(:ldap_filter) { nil } +end From e6d50f635752e98cbb2d4ae31db6b0c68a8eba8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 31 Mar 2022 10:14:46 +0200 Subject: [PATCH 12/60] Extend attachments drop helper to allow a stopover --- .../attachments/attachment_upload_spec.rb | 12 +++ .../components/attachments/attachments.rb | 18 +++- .../attachments/attachments_input.js | 100 ++++++++++++------ 3 files changed, 92 insertions(+), 38 deletions(-) diff --git a/spec/features/work_packages/attachments/attachment_upload_spec.rb b/spec/features/work_packages/attachments/attachment_upload_spec.rb index 8188316031..d1f6d653ee 100644 --- a/spec/features/work_packages/attachments/attachment_upload_spec.rb +++ b/spec/features/work_packages/attachments/attachment_upload_spec.rb @@ -182,6 +182,18 @@ describe 'Upload attachment to work package', js: true do end describe 'attachment dropzone' do + it 'can drag something to the files tab and have it open' do + wp_page.expect_tab 'Activity' + attachments.drag_and_drop_file '.wp-attachment-upload', + image_fixture.path, + :center, + page.find('[data-qa-tab-id="files"]') + + expect(page).to have_selector('.work-package--attachments--filename', text: 'image.png', wait: 10) + expect(page).not_to have_selector('op-toasters-upload-progress') + wp_page.expect_tab 'Files' + end + it 'can upload an image via attaching and drag & drop' do wp_page.switch_to_tab(tab: 'files') container = page.find('.wp-attachment-upload') diff --git a/spec/support/components/attachments/attachments.rb b/spec/support/components/attachments/attachments.rb index f1d1e034ff..0a7e014cd7 100644 --- a/spec/support/components/attachments/attachments.rb +++ b/spec/support/components/attachments/attachments.rb @@ -8,19 +8,27 @@ module Components ## # Drag and Drop the file loaded from path on to the (native) target element - def drag_and_drop_file(target, path, position = :center) + def drag_and_drop_file(target, path, position = :center, stopover = nil) # Remove any previous input, if any page.execute_script <<-JS jQuery('#temporary_attachment_files').remove() JS - # Use the HTML5 file dropper to create a fake drop event - scroll_to_element(target) + element = + if target.is_a?(String) + target + else + # Use the HTML5 file dropper to create a fake drop event + scroll_to_element(target) + target.native + end + page.execute_script( js_drop_files, - target.native, + element, 'temporary_attachment_files', - position.to_s + position.to_s, + stopover ) attach_file_on_input(path, 'temporary_attachment_files') diff --git a/spec/support/components/attachments/attachments_input.js b/spec/support/components/attachments/attachments_input.js index 25216c84d3..f8bc3a3889 100644 --- a/spec/support/components/attachments/attachments_input.js +++ b/spec/support/components/attachments/attachments_input.js @@ -1,33 +1,55 @@ -var params = arguments; +let params = arguments; // Target element to drag & drop to -var target = params[0]; +let target = params[0]; // name of the hidden file input field // must exist on the page, create if needed. -var name = params[1]; - -var position = params[2]; - -// We need coordinates to drop to the element -var box = target.getBoundingClientRect(); -var targetX; -var targetY; - -switch (position) { - case 'center': - targetX = box.left + (box.width / 2); - targetY = box.top + (box.height / 2); - break; - case 'bottom': - targetX = box.left + (box.width / 2); - targetY = box.bottom - 1; - break; - default: - throw new Error("Wrong position " + position); +let name = params[1]; + +let position = params[2]; + +// We might want to drag the file over something, then wait a bit and drag it elsewhere +let stopover = params[3]; + +function dropOnTarget(dataTransfer) { + // Look up the selector + if (typeof target === 'string') { + target = document.querySelector(target); + } + + // We need coordinates to drop to the element + let box = target.getBoundingClientRect(); + let targetX; + let targetY; + + switch (position) { + case 'center': + targetX = box.left + (box.width / 2); + targetY = box.top + (box.height / 2); + break; + case 'bottom': + targetX = box.left + (box.width / 2); + targetY = box.bottom - 1; + break; + default: + throw new Error("Wrong position " + position); + } + + ['dragenter', 'dragover', 'drop'].forEach(function (type) { + let event = new MouseEvent(type, { clientX: targetX, clientY: targetY }); + + // Override the constructor to the DragEvent class + Object.setPrototypeOf(event, null); + event.dataTransfer = dataTransfer; + Object.setPrototypeOf(event, DragEvent.prototype); + + console.log("Dispatching event %O", event); + target.dispatchEvent(event); + }); } -var input = jQuery('') +let input = jQuery('') .attr('id', name) .attr('name', name) .attr('type', 'file') @@ -37,7 +59,7 @@ var input = jQuery('') input.remove(); event.stopPropagation(); - var dataTransfer = { + let dataTransfer = { constructor : DataTransfer, effectAllowed : 'all', dropEffect : 'none', @@ -49,16 +71,28 @@ var input = jQuery('') setDragImage : function setDragImage(){} }; - ['dragenter', 'dragover', 'drop'].forEach(function (type) { - var event = new MouseEvent(type, { clientX: targetX, clientY: targetY }); + // If we have a stopover, do that first and then get the target + if (stopover) { + // We need coordinates to drop to the element + let stopbox = stopover.getBoundingClientRect(); + let stopX; + let stopY; + + stopX = stopbox.left + (stopbox.width / 2); + stopY = stopbox.top + (stopbox.height / 2); + + ['dragenter', 'dragover'].forEach(function (type) { + let event = new MouseEvent(type, { clientX: stopX, clientY: stopY }); + // Override the constructor to the DragEvent class + Object.setPrototypeOf(event, null); + event.dataTransfer = dataTransfer; + Object.setPrototypeOf(event, DragEvent.prototype); - // Override the constructor to the DragEvent class - Object.setPrototypeOf(event, null); - event.dataTransfer = dataTransfer; - Object.setPrototypeOf(event, DragEvent.prototype); + console.log("Dispatching event %O", event); + stopover.dispatchEvent(event); + }); + } - console.log("Dispatching event %O", event); - target.dispatchEvent(event); - }); + setTimeout(() => dropOnTarget(dataTransfer), 2000); }); From bbc8c6127e5cf09e285116ccbfadd6d0f29d31e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20B=C3=A4dorf?= Date: Thu, 31 Mar 2022 08:37:56 +0000 Subject: [PATCH 13/60] Update postgres tooling in docker dev (#10375) * Update postgres tooling in docker dev * Don't use github ffi --- Gemfile | 2 ++ Gemfile.lock | 1 + docker/dev/backend/Dockerfile | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 9ad3774e19..117ec13637 100644 --- a/Gemfile +++ b/Gemfile @@ -37,6 +37,8 @@ gem 'activerecord-session_store', '~> 2.0.0' gem 'rails', '~> 6.1.4', '>= 6.1.4.6' gem 'responders', '~> 3.0' +gem 'ffi', '~> 1.15' + gem 'rdoc', '>= 2.4.2' gem 'doorkeeper', '~> 5.5.0' diff --git a/Gemfile.lock b/Gemfile.lock index 807c1c64e4..faa2bee799 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1018,6 +1018,7 @@ DEPENDENCIES factory_bot (~> 6.2.0) factory_bot_rails (~> 6.2.0) faker + ffi (~> 1.15) flamegraph fog-aws friendly_id (~> 5.4.0) diff --git a/docker/dev/backend/Dockerfile b/docker/dev/backend/Dockerfile index e972a747a3..bf24d5171a 100644 --- a/docker/dev/backend/Dockerfile +++ b/docker/dev/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.0.3-buster as develop +FROM ruby:3.0.3-bullseye as develop MAINTAINER operations@openproject.com ARG DEV_UID=1000 @@ -23,7 +23,7 @@ WORKDIR /home/$USER RUN apt-get update -qq && \ DEBIAN_FRONTEND=noninteractive apt-get install -y \ - postgresql-client + postgresql-client libffi7 libffi-dev COPY ./docker/dev/backend/scripts/setup /usr/sbin/setup COPY ./docker/dev/backend/scripts/run-app /usr/sbin/run-app From 01f99785c595f2861fafbd55c2e14f211b1f385d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 31 Mar 2022 10:41:27 +0200 Subject: [PATCH 14/60] Add spec to drop something on activity --- .../work-package-comment.component.html | 1 + .../attachments/attachment_upload_spec.rb | 13 ++++++++++++- .../components/attachments/attachments_input.js | 5 ++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/features/work-packages/components/work-package-comment/work-package-comment.component.html b/frontend/src/app/features/work-packages/components/work-package-comment/work-package-comment.component.html index 373e08371f..6e69ca0788 100644 --- a/frontend/src/app/features/work-packages/components/work-package-comment/work-package-comment.component.html +++ b/frontend/src/app/features/work-packages/components/work-package-comment/work-package-comment.component.html @@ -19,6 +19,7 @@
      @@ -53,7 +55,7 @@
      + [fieldName]="descriptor.name">
      @@ -84,9 +86,9 @@
      + [resource]="workPackage" + [isDropTarget]="true" + [wrapperClasses]="'-no-label'">
      @@ -102,7 +104,7 @@ + query: group.query }"> @@ -140,3 +142,23 @@ + +
      +
      +
      +
      +

      +
      +
      + +
      + + + +
      +
      +
      diff --git a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts index 85eda07680..e041f91f82 100644 --- a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts @@ -29,7 +29,7 @@ import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { StateService } from '@uirouter/core'; import { Component, Injector, OnInit } from '@angular/core'; -import { Observable, of } from 'rxjs'; +import { of } from 'rxjs'; import { WorkPackageViewSelectionService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-selection.service'; import { WorkPackageSingleViewBase } from 'core-app/features/work-packages/routing/wp-view-base/work-package-single-view.base'; import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service'; diff --git a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.html b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.html index 838a987c94..747e0a31a4 100644 --- a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.html +++ b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.html @@ -3,78 +3,78 @@ *ngIf="workPackage" class="work-packages--show-view"> - + -
      -
      -
      +
      +
      +
      - - + + -
      - -
      +
      +
      -
        -
      • - - -
      • -
      • - - -
      • -
      • - - -
      • -
      • - -
      • -
      +
        +
      • + + +
      • +
      • + + +
      • +
      • + + +
      • +
      • + +
      • +
      +
      -
      -
      -
      - -
      +
      +
      +
      +
      -
      -
      - - -
      - -
      -
      -
      +
      +
      +
      + + +
      +
      - -
      - +
      + +
      + +
      +
      diff --git a/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.html b/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.html index 81585e0f7b..4c3ae75f31 100644 --- a/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.html +++ b/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.html @@ -3,8 +3,8 @@ data-indicator-name="wpDetails" *ngIf="workPackage" > - -
      + +
      - -
      -
      - + +
      +
      + -
      - +
      + - -
      +
      - -
      -
      - -
      + +
      + +
      + +
      -
      - -
      +
      + +
      -
      - -
      +
      + +
      diff --git a/frontend/src/global_styles/content/_attributes_group.sass b/frontend/src/global_styles/content/_attributes_group.sass index 85455f7a37..991a3259c1 100644 --- a/frontend/src/global_styles/content/_attributes_group.sass +++ b/frontend/src/global_styles/content/_attributes_group.sass @@ -33,7 +33,7 @@ .attributes-group--header @include grid-block - margin: 0 0 0.5rem 0 + margin: 0 0 0.5rem 0 border-bottom: 1px solid #ddd align-items: flex-end @@ -61,6 +61,13 @@ .button margin: 0 0 8px 0 +.attributes-group--icon-indented-text + display: grid + margin-top: 1rem + grid-template-columns: auto auto 1fr + column-gap: 8px + + // HACK. TODO: Remove H3 element rules in various places. .attributes-group--header-text, #content h3.attributes-group--header-text From c991d6771cb67e159f630354d703cda2281a5efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 31 Mar 2022 11:24:01 +0200 Subject: [PATCH 16/60] Readd the two removed rake tasks as services They were broken so I removed them. But instead of removing import functionality, I decided to re-add them and fix them. --- app/models/ldap_auth_source.rb | 13 ++- app/services/ldap/base_service.rb | 103 ++++++++++++++++++ .../ldap/import_users_from_filter_service.rb | 30 +++++ .../ldap/import_users_from_list_service.rb | 35 ++++++ .../ldap/synchronize_users_service.rb | 55 +--------- lib/tasks/ldap.rake | 32 +++++- ...port_user_list_service_integration_spec.rb | 54 +++++++++ ...rs_from_filter_service_integration_spec.rb | 26 +++++ 8 files changed, 292 insertions(+), 56 deletions(-) create mode 100644 app/services/ldap/base_service.rb create mode 100644 app/services/ldap/import_users_from_filter_service.rb create mode 100644 app/services/ldap/import_users_from_list_service.rb create mode 100644 spec/services/ldap/import_user_list_service_integration_spec.rb create mode 100644 spec/services/ldap/import_users_from_filter_service_integration_spec.rb diff --git a/app/models/ldap_auth_source.rb b/app/models/ldap_auth_source.rb index 545e0d23a5..112cd1f271 100644 --- a/app/models/ldap_auth_source.rb +++ b/app/models/ldap_auth_source.rb @@ -119,6 +119,13 @@ class LdapAuthSource < AuthSource parsed_filter_string || object_filter end + ## + # Returns the filter object to search for a login + # adding the optional default filter + def login_filter(login) + Net::LDAP::Filter.eq(attr_login, login) & default_filter + end + def parsed_filter_string Net::LDAP::Filter.from_rfc2254(filter_string) if filter_string.present? end @@ -162,15 +169,15 @@ class LdapAuthSource < AuthSource # Get the user's dn and any attributes for them, given their login def get_user_dn(login) ldap_con = initialize_ldap_con(account, account_password) - login_filter = Net::LDAP::Filter.eq(attr_login, login) attrs = {} + filter = login_filter(login) Rails.logger.debug do - "LDAP initializing search (BASE=#{base_dn}), (FILTER=#{default_filter & login_filter})" + "LDAP initializing search (BASE=#{base_dn}), (FILTER=#{filter})" end ldap_con.search(base: base_dn, - filter: default_filter & login_filter, + filter: filter, attributes: search_attributes) do |entry| attrs = if onthefly_register? get_user_attributes_from_ldap_entry(entry) diff --git a/app/services/ldap/base_service.rb b/app/services/ldap/base_service.rb new file mode 100644 index 0000000000..8be5725619 --- /dev/null +++ b/app/services/ldap/base_service.rb @@ -0,0 +1,103 @@ +module Ldap + class BaseService + attr_reader :ldap + + def initialize(ldap) + @ldap = ldap + end + + def call + User.system.run_given do + OpenProject::Mutex.with_advisory_lock_transaction(ldap, 'import_users') do + perform + end + end + end + + def perform + raise NotImplementedError + end + + # rubocop:disable Metrics/AbcSize + def synchronize_user(user, ldap_con) + Rails.logger.debug { "[LDAP user sync] Synchronizing user #{user.login}." } + + update_attributes = user_attributes(user.login, ldap_con) + if update_attributes.nil? && user.persisted? + Rails.logger.info { "Could not find user #{user.login} in #{ldap.name}. Locking the user." } + user.update_column(:status, Principal.statuses[:locked]) + end + return unless update_attributes + + if user.new_record? + try_to_create(update_attributes) + else + try_to_update(user, update_attributes) + end + end + # rubocop:enable Metrics/AbcSize + + # Try to create the user from attributes + def try_to_update(user, attrs) + call = Users::UpdateService + .new(model: user, user: User.system) + .call(attrs) + + if call.success? + # Ensure the user is activated + call.result.update_column(:status, Principal.statuses[:active]) + Rails.logger.info { "[LDAP user sync] User '#{call.result.login}' updated." } + else + Rails.logger.error { "[LDAP user sync] User '#{user.login}' could not be updated: #{call.message}" } + end + end + + def try_to_create(attrs) + call = Users::CreateService + .new(user: User.system) + .call(attrs) + + if call.success? + Rails.logger.info { "[LDAP user sync] User '#{call.result.login}' created." } + else + Rails.logger.error { "[LDAP user sync] User '#{attrs[:login]}' could not be created: #{call.message}" } + end + end + + ## + # Get the user attributes of a single matching LDAP entry. + # + # If the login matches multiple entries, return nil and issue a warning. + # If the login does not match, returns nil + def user_attributes(login, ldap_con) + # Return the first matching user + entries = find_entries_by(login: login, ldap_con: ldap_con) + + if entries.count == 0 + Rails.logger.info { "[LDAP user sync] Did not find LDAP entry for #{login}" } + return + end + + if entries.count > 1 + Rails.logger.warn { "[LDAP user sync] Found multiple entries for #{login}: #{entries.map(&:dn)}. Skipping" } + return + end + + entries.first + end + + def find_entries_by(login:, ldap_con: new_ldap_connection) + ldap_con + .search( + base: ldap.base_dn, + filter: ldap.login_filter(login), + attributes: ldap.search_attributes(true) + ) + .map { |entry| ldap.get_user_attributes_from_ldap_entry(entry).except(:dn) } + end + + def new_ldap_connection + ldap.instance_eval { initialize_ldap_con(account, account_password) } + end + end +end diff --git a/app/services/ldap/import_users_from_filter_service.rb b/app/services/ldap/import_users_from_filter_service.rb new file mode 100644 index 0000000000..97dbe53777 --- /dev/null +++ b/app/services/ldap/import_users_from_filter_service.rb @@ -0,0 +1,30 @@ +module Ldap + class ImportUsersFromFilterService < BaseService + attr_reader :filter + + def initialize(ldap, filter) + super(ldap) + @filter = filter + end + + def perform + get_entries_from_filter do |entry| + attributes = ldap.get_user_attributes_from_ldap_entry(entry) + next if User.by_login(attributes[:login]).exists? + + try_to_create attributes.except(:dn) + end + end + + def get_entries_from_filter(&block) + ldap_con = new_ldap_connection + + ldap_con.search( + base: ldap.base_dn, + filter: filter & ldap.default_filter, + attributes: ldap.search_attributes(true), + &block + ) + end + end +end diff --git a/app/services/ldap/import_users_from_list_service.rb b/app/services/ldap/import_users_from_list_service.rb new file mode 100644 index 0000000000..05179652f6 --- /dev/null +++ b/app/services/ldap/import_users_from_list_service.rb @@ -0,0 +1,35 @@ +module Ldap + class ImportUsersFromListService < BaseService + attr_reader :logins + + def initialize(ldap, logins) + super(ldap) + @logins = logins + end + + def perform + new_users = logins - existing_users + + Rails.logger.debug { "Importing LDAP user import for #{ldap.name} for #{new_users.count} new users." } + import! new_users + end + + def import!(new_users) + ldap_con = new_ldap_connection + + new_users.each do |login| + synchronize_user(User.new(login: login), ldap_con) + rescue ::AuthSource::Error => e + Rails.logger.error { "Failed to synchronize user #{ldap.name} due to LDAP error: #{e.message}" } + # Reset the LDAP connection + ldap_con = new_ldap_connection + rescue StandardError => e + Rails.logger.error { "Failed to synchronize user #{ldap.name}: #{e.message}" } + end + end + + def existing_users + User.where("LOWER(login) in (?)", logins.map(&:downcase)).pluck(:login) + end + end +end diff --git a/app/services/ldap/synchronize_users_service.rb b/app/services/ldap/synchronize_users_service.rb index 401dd6768a..c3e2e6280a 100644 --- a/app/services/ldap/synchronize_users_service.rb +++ b/app/services/ldap/synchronize_users_service.rb @@ -1,9 +1,9 @@ module Ldap - class SynchronizeUsersService - attr_reader :ldap, :logins + class SynchronizeUsersService < BaseService + attr_reader :logins def initialize(ldap, logins = nil) - @ldap = ldap + super(ldap) @logins = logins end @@ -42,54 +42,5 @@ module Ldap ldap.users end end - - def new_ldap_connection - ldap.instance_eval { initialize_ldap_con(account, account_password) } - end - - def synchronize_user(user, ldap_con) - Rails.logger.debug { "Synchronizing user #{user.login}." } - - update_attributes = user_attributes(user.login, ldap_con) - if update_attributes - try_to_update(user, update_attributes) - else - Rails.logger.info { "Could not find user #{user.login} in #{ldap.name}. Locking the user." } - user.update_column(:status, Principal.statuses[:locked]) - end - end - - # Try to create the user from attributes - # rubocop:disable Metrics/AbcSize - def try_to_update(user, attrs) - call = Users::UpdateService - .new(model: user, user: User.system) - .call(attrs.merge) - - if call.success? - # Ensure the user is activated - call.result.update_column(:status, Principal.statuses[:active]) - Rails.logger.info { "[LDAP user sync] User '#{call.result.login}' updated" } - else - Rails.logger.error { "[LDAP user sync] User '#{user.login}' could not be updated: #{call.message}" } - end - end - # rubocop:enable Metrics/AbcSize - - def user_attributes(login, ldap_con) - # Get user login attribute and base dn which are private - base_dn = ldap.base_dn - - search_attributes = ldap.search_attributes(true) - login_filter = Net::LDAP::Filter.eq(ldap.attr_login, login) - ldap_con.search(base: base_dn, - filter: ldap.default_filter & login_filter, - attributes: search_attributes) do |entry| - data = ldap.get_user_attributes_from_ldap_entry(entry) - return data.except(:dn) - end - - nil - end end end diff --git a/lib/tasks/ldap.rake b/lib/tasks/ldap.rake index ef372b9888..039683da97 100644 --- a/lib/tasks/ldap.rake +++ b/lib/tasks/ldap.rake @@ -39,7 +39,7 @@ namespace :ldap do end desc 'Synchronize existing users from the LDAP auth source' \ - 'rake ldap:sync["name=", users=]' + 'rake ldap:sync name="" users=' task sync: :environment do args = parse_args ldap = LdapAuthSource.find_by!(name: args.fetch(:name)) @@ -50,6 +50,36 @@ namespace :ldap do .call end + desc 'Synchronize users from the LDAP auth source with an optional filter.' \ + 'Note: If you omit the filter, ALL users are imported.' \ + 'rake ldap:import_from_filter name="" filter=' + task import_from_filter: :environment do + args = parse_args + ldap = LdapAuthSource.find_by!(name: args.fetch(:name)) + + # Parse filter string if available + filter = Net::LDAP::Filter.from_rfc2254 args.fetch(:filter, 'objectClass = *') + + ::Ldap::ImportUsersFromFilterService + .new(ldap, filter) + .call + end + + desc 'Synchronize a list of user logins with the LDAP auth source' \ + 'rake ldap:import_from_user_list name=" users=' + task import_from_user_list: :environment do + args = parse_args + ldap = LdapAuthSource.find_by!(name: args.fetch(:name)) + file = args.fetch(:users) + + puts "--> Reading username file #{file}" + users = File.read(file).lines(chomp: true) + + ::Ldap::ImportUsersFromListService + .new(ldap, users) + .call + end + desc 'Register a LDAP auth source for the given LDAP URL and attribute mapping: ' \ 'rake ldap:register["url= name= onthefly=map_{login,firstname,lastname,mail,admin}=attribute"]' task register: :environment do diff --git a/spec/services/ldap/import_user_list_service_integration_spec.rb b/spec/services/ldap/import_user_list_service_integration_spec.rb new file mode 100644 index 0000000000..7b5100652e --- /dev/null +++ b/spec/services/ldap/import_user_list_service_integration_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Ldap::ImportUsersFromListService do + include_context 'with temporary LDAP' + + subject do + described_class.new(auth_source, user_list).call + end + + let(:user_list) do + %w[aa729 bb459 cc414] + end + + it 'adds all three users' do + subject + + user_aa729 = User.find_by(login: 'aa729') + expect(user_aa729).to be_present + expect(user_aa729.firstname).to eq 'Alexandra' + expect(user_aa729.lastname).to eq 'Adams' + + user_bb459 = User.find_by(login: 'bb459') + expect(user_bb459).to be_present + expect(user_bb459.firstname).to eq 'Belle' + expect(user_bb459.lastname).to eq 'Baldwin' + + user_cc414 = User.find_by(login: 'cc414') + expect(user_cc414).to be_present + expect(user_cc414.firstname).to eq 'Claire' + expect(user_cc414.lastname).to eq 'Carpenter' + end + + context 'when two users already exist' do + let!(:user_aa729) { create :user, login: 'aa729', firstname: 'Foobar', auth_source: auth_source } + let!(:user_bb459) { create :user, login: 'bb459', firstname: 'Bla', auth_source: auth_source } + + it 'adds the third one, but does not update the other two' do + subject + + user_aa729.reload + user_bb459.reload + + expect(user_aa729.firstname).to eq 'Foobar' + expect(user_aa729.lastname).to eq 'Bobbit' + expect(user_bb459.firstname).to eq 'Bla' + expect(user_bb459.lastname).to eq 'Bobbit' + + user_cc414 = User.find_by(login: 'cc414') + expect(user_cc414).to be_present + expect(user_cc414.firstname).to eq 'Claire' + expect(user_cc414.lastname).to eq 'Carpenter' + end + end +end diff --git a/spec/services/ldap/import_users_from_filter_service_integration_spec.rb b/spec/services/ldap/import_users_from_filter_service_integration_spec.rb new file mode 100644 index 0000000000..ed27e938b2 --- /dev/null +++ b/spec/services/ldap/import_users_from_filter_service_integration_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Ldap::ImportUsersFromFilterService do + include_context 'with temporary LDAP' + + subject do + described_class.new(auth_source, filter).call + end + + let(:filter) { Net::LDAP::Filter.from_rfc2254 '(uid=aa729)' } + + it 'adds only the matching user' do + subject + + user_aa729 = User.find_by(login: 'aa729') + expect(user_aa729).to be_present + expect(user_aa729.firstname).to eq 'Alexandra' + expect(user_aa729.lastname).to eq 'Adams' + + user_bb459 = User.find_by(login: 'bb459') + expect(user_bb459).not_to be_present + + user_cc414 = User.find_by(login: 'cc414') + expect(user_cc414).not_to be_present + end +end From ff0d4c75e6fac4bcf7e3026c07d19ab6593e37ae Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Thu, 31 Mar 2022 13:30:58 +0200 Subject: [PATCH 17/60] [chore] added tests for both locations --- .../work_packages/tabs/activity_tab_spec.rb | 61 +++++++++++---- .../work_packages/tabs/files_tab_spec.rb | 75 +++++++++++++++++++ 2 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 spec/features/work_packages/tabs/files_tab_spec.rb diff --git a/spec/features/work_packages/tabs/activity_tab_spec.rb b/spec/features/work_packages/tabs/activity_tab_spec.rb index 4d38706fc9..3d2fb1e22e 100644 --- a/spec/features/work_packages/tabs/activity_tab_spec.rb +++ b/spec/features/work_packages/tabs/activity_tab_spec.rb @@ -1,3 +1,31 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2022 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + require 'spec_helper' require 'features/work_packages/work_packages_page' @@ -34,7 +62,7 @@ describe 'Activity tab', js: true, selenium: true do work_package.journals[0] end - let!(:note_1) do + let!(:note1) do attributes = { subject: 'New subject', description: 'Some not so long description.' } alter_work_package_at(work_package, @@ -45,12 +73,12 @@ describe 'Activity tab', js: true, selenium: true do work_package.journals.last end - let!(:note_2) do + let!(:note2) do attributes = { journal_notes: 'Another comment by a different user' } alter_work_package_at(work_package, attributes: attributes, - at: 1.days.ago.to_date.to_s(:db), + at: 1.day.ago.to_date.to_s(:db), user: create(:admin)) work_package.journals.last @@ -65,7 +93,7 @@ describe 'Activity tab', js: true, selenium: true do shared_examples 'shows activities in order' do let(:journals) do - journals = [initial_note, note_1, note_2] + journals = [initial_note, note1, note2] journals end @@ -87,12 +115,12 @@ describe 'Activity tab', js: true, selenium: true do activity = page.find("#activity-#{idx + 1}") - if journal.id != note_1.id + if journal.id != note1.id expect(activity).to have_selector('.op-user-activity--user-line', text: journal.user.name) expect(activity).to have_selector('.user-comment > .message', text: journal.notes, visible: :all) end - if activity == note_1 + if activity == note1 expect(activity).to have_selector('.work-package-details-activities-messages .message', count: 2) expect(activity).to have_selector('.message', @@ -113,8 +141,7 @@ describe 'Activity tab', js: true, selenium: true do context 'with permission' do let(:role) do - create(:role, permissions: %i[view_work_packages - add_work_package_notes]) + create(:role, permissions: %i[view_work_packages add_work_package_notes]) end let(:user) do create(:user, @@ -124,27 +151,29 @@ describe 'Activity tab', js: true, selenium: true do context 'with ascending comments' do let(:comments_in_reverse) { false } + it_behaves_like 'shows activities in order' end context 'with reversed comments' do let(:comments_in_reverse) { true } + it_behaves_like 'shows activities in order' end it 'can deep link to an activity' do - visit "/work_packages/#{work_package.id}/activity#activity-#{note_2.id}" + visit "/work_packages/#{work_package.id}/activity#activity-#{note2.id}" work_package_page.ensure_page_loaded expect(page).to have_selector('.user-comment > .message', text: initial_comment) - expect(page.current_url).to match /\/work_packages\/#{work_package.id}\/activity#activity-#{note_2.id}/ + expect(page.current_url).to match /\/work_packages\/#{work_package.id}\/activity#activity-#{note2.id}/ end it 'can toggle between activities and comments-only' do expect(page).to have_selector('.work-package-details-activities-activity-contents', count: 3) - expect(page).to have_selector('.user-comment > .message', text: note_2.notes) + expect(page).to have_selector('.user-comment > .message', text: note2.notes) # Show only comments find('.activity-comments--toggler').click @@ -152,7 +181,7 @@ describe 'Activity tab', js: true, selenium: true do # It should remove the middle expect(page).to have_selector('.work-package-details-activities-activity-contents', count: 2) expect(page).to have_selector('.user-comment > .message', text: initial_comment) - expect(page).to have_selector('.user-comment > .message', text: note_2.notes) + expect(page).to have_selector('.user-comment > .message', text: note2.notes) # Show all again find('.activity-comments--toggler').click @@ -190,18 +219,20 @@ describe 'Activity tab', js: true, selenium: true do end it 'shows the activities, but does not allow commenting' do - expect(page).not_to have_selector('.work-packages--activity--add-comment', visible: true) + expect(page).not_to have_selector('.work-packages--activity--add-comment', visible: :visible) end end end - context 'split screen' do + context 'if on split screen' do let(:work_package_page) { Pages::SplitWorkPackage.new(work_package, project) } + it_behaves_like 'activity tab' end - context 'full screen' do + context 'if on full screen' do let(:work_package_page) { Pages::FullWorkPackage.new(work_package) } + it_behaves_like 'activity tab' end end diff --git a/spec/features/work_packages/tabs/files_tab_spec.rb b/spec/features/work_packages/tabs/files_tab_spec.rb new file mode 100644 index 0000000000..dc758ccfec --- /dev/null +++ b/spec/features/work_packages/tabs/files_tab_spec.rb @@ -0,0 +1,75 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2022 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' + +describe 'Files tab', js: true do + let(:role) { create(:role, permissions: %i[view_work_packages edit_work_packages]) } + let(:user) { create(:user, member_in_project: project, member_through_role: role) } + let(:project) { create :project } + let(:work_package) { create(:work_package, project: project) } + let(:wp_page) { ::Pages::FullWorkPackage.new(work_package, project) } + + before do + login_as(user) + end + + describe 'navigation to new files tab from work package view' do + before do + wp_page.visit! + end + + context 'if on work packages full view' do + it 'must open files tab' do + wp_page.switch_to_tab tab: 'activity' + expect(page).not_to have_selector '.work-package--attachments--drop-box' + + files_link = wp_page.find('.work-packages--files-container .attributes-group--icon-indented-text a') + files_link.click + + expect(page).to have_current_path project_work_package_path(project, work_package, 'files') + expect(page).to have_selector '.work-package--attachments--drop-box' + end + end + + context 'if on work packages split view' do + let(:wp_page) { ::Pages::SplitWorkPackage.new(work_package, project) } + + it 'must open files tab' do + wp_page.switch_to_tab tab: 'overview' + expect(page).not_to have_selector '.work-package--attachments--drop-box' + + files_link = wp_page.find('.work-packages--files-container .attributes-group--icon-indented-text a') + files_link.click + + expect(page).to have_current_path project_work_packages_path(project) + "/details/#{work_package.id}/files" + expect(page).to have_selector '.work-package--attachments--drop-box' + end + end + end +end From 0dbae41624f0ed6df7163ddcf0454c35d27cd081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20B=C3=A4dorf?= Date: Mon, 28 Mar 2022 10:20:54 +0200 Subject: [PATCH 18/60] Fix datepicker overflow by showing scrollbar --- frontend/src/global_styles/vendor/_flatpickr-overrides.sass | 4 ++++ frontend/src/global_styles/vendor/_index.sass | 1 + 2 files changed, 5 insertions(+) create mode 100644 frontend/src/global_styles/vendor/_flatpickr-overrides.sass diff --git a/frontend/src/global_styles/vendor/_flatpickr-overrides.sass b/frontend/src/global_styles/vendor/_flatpickr-overrides.sass new file mode 100644 index 0000000000..34406f709c --- /dev/null +++ b/frontend/src/global_styles/vendor/_flatpickr-overrides.sass @@ -0,0 +1,4 @@ +.flatpickr-innerContainer + + @at-root div#{&} + overflow: auto diff --git a/frontend/src/global_styles/vendor/_index.sass b/frontend/src/global_styles/vendor/_index.sass index e235b0b4e4..c05e86fdeb 100644 --- a/frontend/src/global_styles/vendor/_index.sass +++ b/frontend/src/global_styles/vendor/_index.sass @@ -4,3 +4,4 @@ @import enjoyhint @import ngselect @import full_calendar +@import flatpickr-overrides From 01a2af5b6d9e08a2278f6d205e603aa7cf0fb749 Mon Sep 17 00:00:00 2001 From: slauffer <38492357+slauffer@users.noreply.github.com> Date: Thu, 31 Mar 2022 11:27:57 +0200 Subject: [PATCH 19/60] Update meeting_mailer.rb When using the METHOD in in multipart mime content-type mail here we need to set the method in the ics file, too. See rfc2445, chapter "4.7.2 Method". We noticed this problem without this METHOD:PUBLISH ics part in the Horde Groupware. --- modules/meeting/app/mailers/meeting_mailer.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/meeting/app/mailers/meeting_mailer.rb b/modules/meeting/app/mailers/meeting_mailer.rb index 52a283b872..5b94a6f802 100644 --- a/modules/meeting/app/mailers/meeting_mailer.rb +++ b/modules/meeting/app/mailers/meeting_mailer.rb @@ -77,7 +77,10 @@ class MeetingMailer < UserMailer e.uid = "#{@meeting.id}@#{@meeting.project.identifier}" e.organizer = author end - + + # add needed 'METHOD:PUBLISH' to ics file + entry.publish + attachments['meeting.ics'] = entry.to_ical mail(to: user.mail, subject: subject) end From 87ae535d07480cbc740a301628025b525f30d5bc Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Thu, 31 Mar 2022 15:02:58 +0200 Subject: [PATCH 20/60] [chore] fixed spec expectation --- spec/features/types/form_configuration_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/types/form_configuration_spec.rb b/spec/features/types/form_configuration_spec.rb index b12a245677..9728212c21 100644 --- a/spec/features/types/form_configuration_spec.rb +++ b/spec/features/types/form_configuration_spec.rb @@ -118,7 +118,7 @@ describe 'form configuration', type: :feature, js: true do wp_page.expect_hidden_field(:done_ratio) groups = page.all('.attributes-group--header-text').map(&:text) - expect(groups).to eq [] + expect(groups).to eq ['FILES'] expect(page) .to have_selector('.work-packages--details--description', text: work_package.description) end From 1ed2502d70a3d1041cc6c94b60d3198c1e86a189 Mon Sep 17 00:00:00 2001 From: Eric Schubert <38206611+Kharonus@users.noreply.github.com> Date: Thu, 31 Mar 2022 15:10:25 +0200 Subject: [PATCH 21/60] Update frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.html MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit multi line html element attributes Co-authored-by: Benjamin Bädorf --- .../tabs/scrollable-tabs/scrollable-tabs.component.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.html b/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.html index 53d84ace64..65710d0f15 100644 --- a/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.html +++ b/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.html @@ -3,7 +3,11 @@ #scrollContainer class="op-scrollable-tabs--tab-container" > -
        +
        • Date: Fri, 1 Apr 2022 03:26:50 +0000 Subject: [PATCH 22/60] update locales from crowdin [ci skip] --- config/locales/crowdin/js-af.yml | 1 + config/locales/crowdin/js-ar.yml | 1 + config/locales/crowdin/js-az.yml | 1 + config/locales/crowdin/js-bg.yml | 1 + config/locales/crowdin/js-ca.yml | 1 + config/locales/crowdin/js-cs.yml | 1 + config/locales/crowdin/js-da.yml | 1 + config/locales/crowdin/js-de.yml | 1 + config/locales/crowdin/js-el.yml | 1 + config/locales/crowdin/js-eo.yml | 1 + config/locales/crowdin/js-es.yml | 1 + config/locales/crowdin/js-et.yml | 1 + config/locales/crowdin/js-fa.yml | 1 + config/locales/crowdin/js-fi.yml | 1 + config/locales/crowdin/js-fil.yml | 1 + config/locales/crowdin/js-fr.yml | 1 + config/locales/crowdin/js-he.yml | 1 + config/locales/crowdin/js-hi.yml | 1 + config/locales/crowdin/js-hr.yml | 1 + config/locales/crowdin/js-hu.yml | 1 + config/locales/crowdin/js-id.yml | 1 + config/locales/crowdin/js-it.yml | 1 + config/locales/crowdin/js-ja.yml | 1 + config/locales/crowdin/js-ko.yml | 1 + config/locales/crowdin/js-lol.yml | 1 + config/locales/crowdin/js-lt.yml | 3 ++- config/locales/crowdin/js-lv.yml | 1 + config/locales/crowdin/js-ne.yml | 1 + config/locales/crowdin/js-nl.yml | 1 + config/locales/crowdin/js-no.yml | 1 + config/locales/crowdin/js-pl.yml | 1 + config/locales/crowdin/js-pt.yml | 1 + config/locales/crowdin/js-ro.yml | 1 + config/locales/crowdin/js-ru.yml | 1 + config/locales/crowdin/js-rw.yml | 1 + config/locales/crowdin/js-si.yml | 1 + config/locales/crowdin/js-sk.yml | 1 + config/locales/crowdin/js-sl.yml | 1 + config/locales/crowdin/js-sv.yml | 1 + config/locales/crowdin/js-th.yml | 1 + config/locales/crowdin/js-tr.yml | 1 + config/locales/crowdin/js-uk.yml | 1 + config/locales/crowdin/js-vi.yml | 1 + config/locales/crowdin/js-zh-TW.yml | 1 + modules/team_planner/config/locales/crowdin/js-lt.yml | 2 +- 45 files changed, 46 insertions(+), 2 deletions(-) diff --git a/config/locales/crowdin/js-af.yml b/config/locales/crowdin/js-af.yml index 918eb42d22..9660510921 100644 --- a/config/locales/crowdin/js-af.yml +++ b/config/locales/crowdin/js-af.yml @@ -996,6 +996,7 @@ af: relations: Relations watchers: Dophouers files: Lêers + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "dae" weeks: "weeks" diff --git a/config/locales/crowdin/js-ar.yml b/config/locales/crowdin/js-ar.yml index 600483d62a..9a7f3e8642 100644 --- a/config/locales/crowdin/js-ar.yml +++ b/config/locales/crowdin/js-ar.yml @@ -1000,6 +1000,7 @@ ar: relations: صِلات watchers: المشاهدون files: الملفّات + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "أيام" weeks: "أسابيع" diff --git a/config/locales/crowdin/js-az.yml b/config/locales/crowdin/js-az.yml index d725f070e0..38fc155dea 100644 --- a/config/locales/crowdin/js-az.yml +++ b/config/locales/crowdin/js-az.yml @@ -996,6 +996,7 @@ az: relations: Relations watchers: Watchers files: Files + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "days" weeks: "weeks" diff --git a/config/locales/crowdin/js-bg.yml b/config/locales/crowdin/js-bg.yml index f53fc7def2..bafe2c70f8 100644 --- a/config/locales/crowdin/js-bg.yml +++ b/config/locales/crowdin/js-bg.yml @@ -996,6 +996,7 @@ bg: relations: Връзки с watchers: Наблюдатели files: Файлове + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "дни" weeks: "седмици" diff --git a/config/locales/crowdin/js-ca.yml b/config/locales/crowdin/js-ca.yml index b91265e3ea..95873061de 100644 --- a/config/locales/crowdin/js-ca.yml +++ b/config/locales/crowdin/js-ca.yml @@ -996,6 +996,7 @@ ca: relations: Relacions watchers: Observadors files: Arxius + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "dies" weeks: "setmanes" diff --git a/config/locales/crowdin/js-cs.yml b/config/locales/crowdin/js-cs.yml index f3b7b8613f..c51897ccb6 100644 --- a/config/locales/crowdin/js-cs.yml +++ b/config/locales/crowdin/js-cs.yml @@ -999,6 +999,7 @@ cs: relations: Vztahy watchers: Sledující files: Soubory + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "dnů" weeks: "týdny" diff --git a/config/locales/crowdin/js-da.yml b/config/locales/crowdin/js-da.yml index 0eb0c3f68e..b2a1c737d8 100644 --- a/config/locales/crowdin/js-da.yml +++ b/config/locales/crowdin/js-da.yml @@ -995,6 +995,7 @@ da: relations: Tilknytninger watchers: Tilsynsførende files: Filer + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "dage" weeks: "uger" diff --git a/config/locales/crowdin/js-de.yml b/config/locales/crowdin/js-de.yml index 3549e091aa..5b83241b7c 100644 --- a/config/locales/crowdin/js-de.yml +++ b/config/locales/crowdin/js-de.yml @@ -995,6 +995,7 @@ de: relations: Beziehungen watchers: Beobachter files: Dateien + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "Tagen" weeks: "Wochen" diff --git a/config/locales/crowdin/js-el.yml b/config/locales/crowdin/js-el.yml index 4e91613413..38c27bef00 100644 --- a/config/locales/crowdin/js-el.yml +++ b/config/locales/crowdin/js-el.yml @@ -995,6 +995,7 @@ el: relations: Συσχετίσεις watchers: Παρατηρητές files: Αρχεία + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "ημέρες" weeks: "εβδομάδες" diff --git a/config/locales/crowdin/js-eo.yml b/config/locales/crowdin/js-eo.yml index dbde9783b0..06ac3b8b62 100644 --- a/config/locales/crowdin/js-eo.yml +++ b/config/locales/crowdin/js-eo.yml @@ -996,6 +996,7 @@ eo: relations: Rilatoj watchers: Atentantoj files: Dosieroj + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "tagoj" weeks: "weeks" diff --git a/config/locales/crowdin/js-es.yml b/config/locales/crowdin/js-es.yml index 3702aa5af4..daba814177 100644 --- a/config/locales/crowdin/js-es.yml +++ b/config/locales/crowdin/js-es.yml @@ -996,6 +996,7 @@ es: relations: Relaciones watchers: Controladores files: Archivos + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "días" weeks: "semanas" diff --git a/config/locales/crowdin/js-et.yml b/config/locales/crowdin/js-et.yml index a16df8e0fa..3a8adab723 100644 --- a/config/locales/crowdin/js-et.yml +++ b/config/locales/crowdin/js-et.yml @@ -996,6 +996,7 @@ et: relations: Seosed watchers: Jälgijad files: Failid + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "päeva" weeks: "nädalat" diff --git a/config/locales/crowdin/js-fa.yml b/config/locales/crowdin/js-fa.yml index 756c9aae65..56c014a057 100644 --- a/config/locales/crowdin/js-fa.yml +++ b/config/locales/crowdin/js-fa.yml @@ -996,6 +996,7 @@ fa: relations: Relations watchers: Watchers files: Files + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "days" weeks: "weeks" diff --git a/config/locales/crowdin/js-fi.yml b/config/locales/crowdin/js-fi.yml index ff469613bc..2e682e93c7 100644 --- a/config/locales/crowdin/js-fi.yml +++ b/config/locales/crowdin/js-fi.yml @@ -996,6 +996,7 @@ fi: relations: Riippuvuudet watchers: Seuraajat files: Tiedostot + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "päivää" weeks: "viikkoa" diff --git a/config/locales/crowdin/js-fil.yml b/config/locales/crowdin/js-fil.yml index d1111d277b..531b1b38d4 100644 --- a/config/locales/crowdin/js-fil.yml +++ b/config/locales/crowdin/js-fil.yml @@ -996,6 +996,7 @@ fil: relations: Mga relasyon watchers: Manonood files: Mga file + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "mga araw" weeks: "mga linggo" diff --git a/config/locales/crowdin/js-fr.yml b/config/locales/crowdin/js-fr.yml index a27404742e..9938fc6a4e 100644 --- a/config/locales/crowdin/js-fr.yml +++ b/config/locales/crowdin/js-fr.yml @@ -996,6 +996,7 @@ fr: relations: Relations watchers: Observateurs files: Fichiers + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "jours" weeks: "semaines" diff --git a/config/locales/crowdin/js-he.yml b/config/locales/crowdin/js-he.yml index 7d0876e328..cfbb28fd93 100644 --- a/config/locales/crowdin/js-he.yml +++ b/config/locales/crowdin/js-he.yml @@ -998,6 +998,7 @@ he: relations: קשרים watchers: צופים files: קבצים + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "ימים" weeks: "שבועות" diff --git a/config/locales/crowdin/js-hi.yml b/config/locales/crowdin/js-hi.yml index 340e0c82ef..18279d014f 100644 --- a/config/locales/crowdin/js-hi.yml +++ b/config/locales/crowdin/js-hi.yml @@ -996,6 +996,7 @@ hi: relations: Relations watchers: वॉचर files: फ़ाइलें + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "days" weeks: "weeks" diff --git a/config/locales/crowdin/js-hr.yml b/config/locales/crowdin/js-hr.yml index 3955dbf658..8715bc54b0 100644 --- a/config/locales/crowdin/js-hr.yml +++ b/config/locales/crowdin/js-hr.yml @@ -997,6 +997,7 @@ hr: relations: Relacije watchers: Nadglednici files: Datoteke + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "dani" weeks: "tjedana" diff --git a/config/locales/crowdin/js-hu.yml b/config/locales/crowdin/js-hu.yml index 4e9a5bee6f..72f2ee876c 100644 --- a/config/locales/crowdin/js-hu.yml +++ b/config/locales/crowdin/js-hu.yml @@ -1001,6 +1001,7 @@ hu: relations: Kapcsolatok watchers: Megfigyelők files: Fájlok + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "nap" weeks: "Hetek" diff --git a/config/locales/crowdin/js-id.yml b/config/locales/crowdin/js-id.yml index b56346a472..dd2bb5ee60 100644 --- a/config/locales/crowdin/js-id.yml +++ b/config/locales/crowdin/js-id.yml @@ -995,6 +995,7 @@ id: relations: Relasi watchers: Pemantau files: File + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "hari" weeks: "minggu" diff --git a/config/locales/crowdin/js-it.yml b/config/locales/crowdin/js-it.yml index a6c461e582..b4091154e3 100644 --- a/config/locales/crowdin/js-it.yml +++ b/config/locales/crowdin/js-it.yml @@ -997,6 +997,7 @@ it: relations: Relazioni watchers: Osservatori files: File + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "giorni" weeks: "Settimane" diff --git a/config/locales/crowdin/js-ja.yml b/config/locales/crowdin/js-ja.yml index 93c15ba7ff..51545fe504 100644 --- a/config/locales/crowdin/js-ja.yml +++ b/config/locales/crowdin/js-ja.yml @@ -998,6 +998,7 @@ ja: relations: 関係 watchers: ウォッチャー files: ファイルを添付する + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "日" weeks: "週" diff --git a/config/locales/crowdin/js-ko.yml b/config/locales/crowdin/js-ko.yml index eac70f0b41..5fd55414c9 100644 --- a/config/locales/crowdin/js-ko.yml +++ b/config/locales/crowdin/js-ko.yml @@ -995,6 +995,7 @@ ko: relations: 관계 watchers: 주시자 files: 파일 + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "일" weeks: "주" diff --git a/config/locales/crowdin/js-lol.yml b/config/locales/crowdin/js-lol.yml index a976f7a97c..9431289c3e 100644 --- a/config/locales/crowdin/js-lol.yml +++ b/config/locales/crowdin/js-lol.yml @@ -995,6 +995,7 @@ lol: relations: crwdns788464:0crwdne788464:0 watchers: crwdns788466:0crwdne788466:0 files: crwdns807702:0crwdne807702:0 + files_tab_migration_help: 'crwdns808154:0crwdne808154:0' time_relative: days: "crwdns788470:0crwdne788470:0" weeks: "crwdns788472:0crwdne788472:0" diff --git a/config/locales/crowdin/js-lt.yml b/config/locales/crowdin/js-lt.yml index 3750c5218b..626d6cc1e1 100644 --- a/config/locales/crowdin/js-lt.yml +++ b/config/locales/crowdin/js-lt.yml @@ -998,6 +998,7 @@ lt: relations: Ryšiai watchers: Stebėtojai files: Failai + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "dienos" weeks: "savaitės" @@ -1171,7 +1172,7 @@ lt: group: 'Grupė jau tapo %{project} dalimi. Tuo pačiu, jūs jau galite planuoti darbus su šia grupe ir pavyzdžiui priskirti jai darbo užduotis.' next_button: 'Tęsti' include_projects: - toggle_title: 'Include projects' + toggle_title: 'Įtraukti projektus' title: 'Projektai' clear_selection: 'Valyti pasirinkimą' apply: 'Taikyti' diff --git a/config/locales/crowdin/js-lv.yml b/config/locales/crowdin/js-lv.yml index c36670c793..7aefb7ddf2 100644 --- a/config/locales/crowdin/js-lv.yml +++ b/config/locales/crowdin/js-lv.yml @@ -997,6 +997,7 @@ lv: relations: Saistītie watchers: Sekotāji files: Faili + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "dienas" weeks: "weeks" diff --git a/config/locales/crowdin/js-ne.yml b/config/locales/crowdin/js-ne.yml index a8a7a6f9db..1382a3769b 100644 --- a/config/locales/crowdin/js-ne.yml +++ b/config/locales/crowdin/js-ne.yml @@ -996,6 +996,7 @@ ne: relations: Relations watchers: Watchers files: Files + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "days" weeks: "weeks" diff --git a/config/locales/crowdin/js-nl.yml b/config/locales/crowdin/js-nl.yml index 7cc963ef82..5c39112270 100644 --- a/config/locales/crowdin/js-nl.yml +++ b/config/locales/crowdin/js-nl.yml @@ -996,6 +996,7 @@ nl: relations: Relaties watchers: Volgers files: Bestanden + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "dagen" weeks: "weken" diff --git a/config/locales/crowdin/js-no.yml b/config/locales/crowdin/js-no.yml index 00fa169942..f54ec7d134 100644 --- a/config/locales/crowdin/js-no.yml +++ b/config/locales/crowdin/js-no.yml @@ -996,6 +996,7 @@ relations: Relasjoner watchers: Overvåkere files: Filer + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "dager" weeks: "uker" diff --git a/config/locales/crowdin/js-pl.yml b/config/locales/crowdin/js-pl.yml index 5758376e1e..24c49b4da1 100644 --- a/config/locales/crowdin/js-pl.yml +++ b/config/locales/crowdin/js-pl.yml @@ -998,6 +998,7 @@ pl: relations: Relacje watchers: Obserwatorzy files: Pliki + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "dni" weeks: "tygodnie" diff --git a/config/locales/crowdin/js-pt.yml b/config/locales/crowdin/js-pt.yml index 693cdf49ed..0c16597ab9 100644 --- a/config/locales/crowdin/js-pt.yml +++ b/config/locales/crowdin/js-pt.yml @@ -995,6 +995,7 @@ pt: relations: Relações watchers: Observadores files: Arquivos + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "dias" weeks: "semanas" diff --git a/config/locales/crowdin/js-ro.yml b/config/locales/crowdin/js-ro.yml index ac3238a20f..9ae7ff5caf 100644 --- a/config/locales/crowdin/js-ro.yml +++ b/config/locales/crowdin/js-ro.yml @@ -996,6 +996,7 @@ ro: relations: Relații watchers: Observatori files: Fişiere + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "zile" weeks: "săptămâni" diff --git a/config/locales/crowdin/js-ru.yml b/config/locales/crowdin/js-ru.yml index 1124d18137..bfbaa600c2 100644 --- a/config/locales/crowdin/js-ru.yml +++ b/config/locales/crowdin/js-ru.yml @@ -997,6 +997,7 @@ ru: relations: Связи watchers: Наблюдатель files: Файлы + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "дней" weeks: "недели" diff --git a/config/locales/crowdin/js-rw.yml b/config/locales/crowdin/js-rw.yml index cbdd6f61d3..b3881d84d9 100644 --- a/config/locales/crowdin/js-rw.yml +++ b/config/locales/crowdin/js-rw.yml @@ -996,6 +996,7 @@ rw: relations: Relations watchers: Watchers files: Files + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "days" weeks: "weeks" diff --git a/config/locales/crowdin/js-si.yml b/config/locales/crowdin/js-si.yml index 6f6132e067..a7098f2722 100644 --- a/config/locales/crowdin/js-si.yml +++ b/config/locales/crowdin/js-si.yml @@ -996,6 +996,7 @@ si: relations: සබඳතා watchers: මුරකරුවන් files: ගොනු + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "දින" weeks: "සති" diff --git a/config/locales/crowdin/js-sk.yml b/config/locales/crowdin/js-sk.yml index 45d2d5c6b8..870fa694c7 100644 --- a/config/locales/crowdin/js-sk.yml +++ b/config/locales/crowdin/js-sk.yml @@ -998,6 +998,7 @@ sk: relations: Väzby watchers: Pozorovatelia files: Súbory + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "dní" weeks: "týždne" diff --git a/config/locales/crowdin/js-sl.yml b/config/locales/crowdin/js-sl.yml index 4f660aea7d..1b6b541292 100644 --- a/config/locales/crowdin/js-sl.yml +++ b/config/locales/crowdin/js-sl.yml @@ -997,6 +997,7 @@ sl: relations: Relacije watchers: Opazovalci files: Datoteke + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "dnevi" weeks: "tednov" diff --git a/config/locales/crowdin/js-sv.yml b/config/locales/crowdin/js-sv.yml index 5abaed46dc..6e1be6f853 100644 --- a/config/locales/crowdin/js-sv.yml +++ b/config/locales/crowdin/js-sv.yml @@ -995,6 +995,7 @@ sv: relations: Relationer watchers: Bevakare files: Filer + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "dagar" weeks: "veckor" diff --git a/config/locales/crowdin/js-th.yml b/config/locales/crowdin/js-th.yml index a3c68eef4f..14dd8e73b5 100644 --- a/config/locales/crowdin/js-th.yml +++ b/config/locales/crowdin/js-th.yml @@ -995,6 +995,7 @@ th: relations: Relations watchers: ผู้เฝ้าดู files: ไฟล์ + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "วัน" weeks: "สัปดาห์" diff --git a/config/locales/crowdin/js-tr.yml b/config/locales/crowdin/js-tr.yml index 1ceb792826..85a569a1a4 100644 --- a/config/locales/crowdin/js-tr.yml +++ b/config/locales/crowdin/js-tr.yml @@ -996,6 +996,7 @@ tr: relations: İlişkiler watchers: Takip Edenler files: Dosyalar + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "gün" weeks: "hafta" diff --git a/config/locales/crowdin/js-uk.yml b/config/locales/crowdin/js-uk.yml index 0b48247918..7385202813 100644 --- a/config/locales/crowdin/js-uk.yml +++ b/config/locales/crowdin/js-uk.yml @@ -998,6 +998,7 @@ uk: relations: Зв'язки watchers: Спостерігачі files: Файли + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "днів" weeks: "тижнів" diff --git a/config/locales/crowdin/js-vi.yml b/config/locales/crowdin/js-vi.yml index 4f2aaa2888..4d5ca58b43 100644 --- a/config/locales/crowdin/js-vi.yml +++ b/config/locales/crowdin/js-vi.yml @@ -994,6 +994,7 @@ vi: relations: Relations watchers: Watchers files: Tập tin + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "ngày" weeks: "weeks" diff --git a/config/locales/crowdin/js-zh-TW.yml b/config/locales/crowdin/js-zh-TW.yml index 4b8f84b3aa..cd440a8fd6 100644 --- a/config/locales/crowdin/js-zh-TW.yml +++ b/config/locales/crowdin/js-zh-TW.yml @@ -994,6 +994,7 @@ zh-TW: relations: 關聯 watchers: 監看者 files: 檔案 + files_tab_migration_help: 'You can now attach files to work packages via the new tab:' time_relative: days: "天" weeks: "週" diff --git a/modules/team_planner/config/locales/crowdin/js-lt.yml b/modules/team_planner/config/locales/crowdin/js-lt.yml index f1157d84b4..bb02b071a8 100644 --- a/modules/team_planner/config/locales/crowdin/js-lt.yml +++ b/modules/team_planner/config/locales/crowdin/js-lt.yml @@ -2,7 +2,7 @@ lt: js: team_planner: - add_existing: 'Add existing' + add_existing: 'Pridėti esamą' create_new: 'Naujas komandos planas' title: 'Komandos planas' unsaved_title: 'Nepavadintas komandos planas' From 0712e496483cb77a7965f8a5a511d1cb47b0b12f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Apr 2022 05:28:59 +0000 Subject: [PATCH 23/60] Bump pg from 1.3.4 to 1.3.5 Bumps [pg](https://github.com/ged/ruby-pg) from 1.3.4 to 1.3.5. - [Release notes](https://github.com/ged/ruby-pg/releases) - [Changelog](https://github.com/ged/ruby-pg/blob/master/History.rdoc) - [Commits](https://github.com/ged/ruby-pg/commits) --- updated-dependencies: - dependency-name: pg dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index faa2bee799..d915b8b6e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -667,7 +667,7 @@ GEM hashery (~> 2.0) ruby-rc4 ttfunk - pg (1.3.4) + pg (1.3.5) plaintext (0.3.4) activesupport (> 2.2.1) nokogiri (~> 1.10, >= 1.10.4) From f4c4544f7ac6ea1b0bdf3c3f790e516686fbdc62 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 1 Apr 2022 11:18:53 +0200 Subject: [PATCH 24/60] bail when trying to write a configuration & deprecte non bool setting_name? access --- app/models/setting.rb | 6 +++++ lib/open_project/configuration.rb | 25 +++++++++++------- spec/lib/open_project/configuration_spec.rb | 26 ++++++++++++++++++ spec/models/setting_spec.rb | 29 ++++++++++++++++++++- 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/app/models/setting.rb b/app/models/setting.rb index 8a7d54c1a8..07861d213d 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -90,6 +90,12 @@ class Setting < ApplicationRecord def self.#{name}? # when running too early, there is no settings table. do nothing return unless settings_table_exists_yet? + definition = Settings::Definition[:#{name}] + + if definition.format != :boolean + ActiveSupport::Deprecation.warn "Calling #{self}.#{name}? is deprecated since it is not a boolean", caller + end + value = self[:#{name}] ActiveRecord::Type::Boolean.new.cast(value) end diff --git a/lib/open_project/configuration.rb b/lib/open_project/configuration.rb index 2b1da81125..6b603f040a 100644 --- a/lib/open_project/configuration.rb +++ b/lib/open_project/configuration.rb @@ -191,28 +191,33 @@ module OpenProject end def method_missing(name, *args, &block) - setting_name = name.to_s.sub(/(=|\?)$/, '') + setting_name = name.to_s.sub(/\?$/, '') - if Settings::Definition.exists?(setting_name) - define_config_methods(setting_name) + definition = Settings::Definition[setting_name] - send(setting_name, *args, &block) + if definition + define_config_methods(definition) + + send(name, *args, &block) else super end end def respond_to_missing?(name, include_private = false) - Settings::Definition.exists?(name.to_s.sub(/(=|\?)$/, '')) || super + Settings::Definition.exists?(name.to_s.sub(/\?$/, '')) || super end - def define_config_methods(setting_name) - define_singleton_method setting_name do - self[setting_name] + def define_config_methods(definition) + define_singleton_method definition.name do + self[definition.name] end - define_singleton_method "#{setting_name}?" do - ['true', true, '1'].include? self[setting_name] + define_singleton_method "#{definition.name}?" do + if definition.format != :boolean + ActiveSupport::Deprecation.warn "Calling #{self}.#{definition.name}? is deprecated since it is not a boolean", caller + end + ['true', true, '1'].include? self[definition.name] end end diff --git a/spec/lib/open_project/configuration_spec.rb b/spec/lib/open_project/configuration_spec.rb index 6df0984141..3da6c540a6 100644 --- a/spec/lib/open_project/configuration_spec.rb +++ b/spec/lib/open_project/configuration_spec.rb @@ -41,6 +41,32 @@ describe OpenProject::Configuration do Setting.clear_cache end + describe '.[setting]' do + it 'fetches the value' do + expect(described_class.app_title) + .to eql('OpenProject') + end + end + + describe '.[setting]?' do + it 'fetches the value' do + expect(described_class.smtp_enable_starttls_auto?) + .to be false + end + + it 'works for non boolean settings as well (deprecated)' do + expect(described_class.app_title?) + .to be false + end + end + + describe '.[setting]=' do + it 'raises an error' do + expect { described_class.smtp_enable_starttls_auto = true } + .to raise_error NoMethodError + end + end + describe '.migrate_mailer_configuration!' do before do allow(Setting) diff --git a/spec/models/setting_spec.rb b/spec/models/setting_spec.rb index 777a7e5844..ae94e99b59 100644 --- a/spec/models/setting_spec.rb +++ b/spec/models/setting_spec.rb @@ -33,7 +33,6 @@ describe Setting, type: :model do described_class.destroy_all end - # OpenProject specific defaults that are set in settings.yml describe "OpenProject's default settings" do it 'has OpenProject as application title' do expect(described_class.app_title).to eq 'OpenProject' @@ -103,6 +102,34 @@ describe Setting, type: :model do end end + describe '.[setting]' do + it 'fetches the value' do + expect(described_class.app_title) + .to eql('OpenProject') + end + end + + describe '.[setting]?' do + it 'fetches the value' do + expect(described_class.smtp_enable_starttls_auto?) + .to be false + end + + it 'works for non boolean settings as well (deprecated)' do + expect(described_class.app_title?) + .to be true + end + end + + describe '.[setting]=' do + it 'sets the value' do + described_class.app_title = 'New title' + + expect(described_class.app_title) + .to eql('New title') + end + end + describe '.[setting]_writable?' do before do allow(Settings::Definition[:host_name]) From 098fcfe59d3b0700dcb57c8ddcb3e7ba645e2749 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 1 Apr 2022 11:22:18 +0200 Subject: [PATCH 25/60] shorten grouping --- config/constants/settings/definition.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config/constants/settings/definition.rb b/config/constants/settings/definition.rb index 7245109f25..44e5fad183 100644 --- a/config/constants/settings/definition.rb +++ b/config/constants/settings/definition.rb @@ -151,8 +151,6 @@ module Settings end def [](name) - by_name ||= all.group_by(&:name).transform_values(&:first) - by_name[name.to_s] end @@ -186,7 +184,7 @@ module Settings end def by_name - @by_name ||= all.group_by(&:name).transform_values(&:first) + @by_name ||= all.index_by(&:name) end def file_config From 07801a8cf880968bbbec5f5a0e8ea61b0489f476 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 1 Apr 2022 14:12:17 +0200 Subject: [PATCH 26/60] raise if non writable setting is attempted to be written --- app/models/setting.rb | 6 ++++-- app/seeders/basic_data/setting_seeder.rb | 2 +- spec/models/setting_spec.rb | 13 +++++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/models/setting.rb b/app/models/setting.rb index 07861d213d..384aec6063 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -156,8 +156,10 @@ class Setting < ApplicationRecord self.class.deserialize(name, read_attribute(:value)) end - def value=(v) - write_attribute(:value, formatted_value(v)) + def value=(val) + raise NoMethodError unless Settings::Definition[name].writable? + + write_attribute(:value, formatted_value(val)) end def formatted_value(value) diff --git a/app/seeders/basic_data/setting_seeder.rb b/app/seeders/basic_data/setting_seeder.rb index a001422a00..b3aa12b03e 100644 --- a/app/seeders/basic_data/setting_seeder.rb +++ b/app/seeders/basic_data/setting_seeder.rb @@ -47,7 +47,7 @@ module BasicData def data @settings ||= begin - settings = Setting.definitions.each_with_object({}) do |definition, hash| + settings = Setting.definitions.select(&:writable?).each_with_object({}) do |definition, hash| hash[definition.name] = definition.value || '' end diff --git a/spec/models/setting_spec.rb b/spec/models/setting_spec.rb index ae94e99b59..92eb7adfca 100644 --- a/spec/models/setting_spec.rb +++ b/spec/models/setting_spec.rb @@ -29,6 +29,10 @@ require 'spec_helper' describe Setting, type: :model do + before do + described_class.clear_cache + end + after do described_class.destroy_all end @@ -128,6 +132,11 @@ describe Setting, type: :model do expect(described_class.app_title) .to eql('New title') end + + it 'raises an error for a non writable setting' do + expect { described_class.smtp_openssl_verify_mode = 'none' } + .to raise_error NoMethodError + end end describe '.[setting]_writable?' do @@ -205,10 +214,6 @@ describe Setting, type: :model do # Check that when reading certain setting values that they get overwritten if needed. describe "filter saved settings" do - before do - described_class.work_package_list_default_highlighting_mode = "inline" - end - describe "with EE token", with_ee: [:conditional_highlighting] do it "returns the value for 'work_package_list_default_highlighting_mode' without changing it" do expect(described_class.work_package_list_default_highlighting_mode).to eq("inline") From f5887d6d14e609b9f6cf1edb2decd08be7ef81a0 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 1 Apr 2022 14:20:15 +0200 Subject: [PATCH 27/60] replace references to settings.yml --- app/seeders/basic_data/setting_seeder.rb | 4 ++-- docs/api/apiv3/tags/configuration.yml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/seeders/basic_data/setting_seeder.rb b/app/seeders/basic_data/setting_seeder.rb index b3aa12b03e..3aca75f85b 100644 --- a/app/seeders/basic_data/setting_seeder.rb +++ b/app/seeders/basic_data/setting_seeder.rb @@ -51,8 +51,8 @@ module BasicData hash[definition.name] = definition.value || '' end - # deviate from the defaults specified in settings.yml here - # to set a default role. The role cannot be specified in the settings.yml as + # deviate from the defaults specified in the settings definition here + # to set a default role. The role cannot be specified in the definition as # that would mean to know the ID upfront. update_unless_present(settings, 'new_project_user_role_id') do Role.find_by(name: I18n.t(:default_role_project_admin)).try(:id) diff --git a/docs/api/apiv3/tags/configuration.yml b/docs/api/apiv3/tags/configuration.yml index 44cc7cb00f..1af5300498 100644 --- a/docs/api/apiv3/tags/configuration.yml +++ b/docs/api/apiv3/tags/configuration.yml @@ -1,7 +1,8 @@ --- description: |- The configuration endpoint allows to read certain configuration parameters of the OpenProject instance. - Note that there is no 1:1 relationship between this endpoint and the settings you can find in your settings.yml. + Note that there is no 1:1 relationship between this endpoint and the settings an administrator has at hand to modify the behaviour + of the application via configuration.yml or ENV variables. For now this endpoint will only allow access to settings deemed useful for a client to know in general. From 17d9d1d76e582f823c909f8bada227e51056fd75 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 1 Apr 2022 14:36:48 +0200 Subject: [PATCH 28/60] add missing saml config definition --- config/constants/settings/definition.rb | 1 + modules/auth_saml/lib/open_project/auth_saml/engine.rb | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/config/constants/settings/definition.rb b/config/constants/settings/definition.rb index 44e5fad183..1246999364 100644 --- a/config/constants/settings/definition.rb +++ b/config/constants/settings/definition.rb @@ -83,6 +83,7 @@ module Settings def override_value(other_value) if format == :hash + self.value = {} if value.nil? value.deep_merge! other_value else self.value = other_value diff --git a/modules/auth_saml/lib/open_project/auth_saml/engine.rb b/modules/auth_saml/lib/open_project/auth_saml/engine.rb index bc865e8476..618950be6b 100644 --- a/modules/auth_saml/lib/open_project/auth_saml/engine.rb +++ b/modules/auth_saml/lib/open_project/auth_saml/engine.rb @@ -83,6 +83,13 @@ module OpenProject end end end + + initializer 'auth_saml.configuration' do + ::Settings::Definition.add 'saml', + value: nil, + format: :hash, + writable: false + end end end end From eb263eaa1771b872610ec0053dd95bc13b82600b Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 1 Apr 2022 14:53:57 +0200 Subject: [PATCH 29/60] replace references to saml settings.yml --- docs/development/saml/README.md | 35 ++++++++++--------- .../authentication/saml/README.md | 24 ++++++------- 2 files changed, 28 insertions(+), 31 deletions(-) diff --git a/docs/development/saml/README.md b/docs/development/saml/README.md index 4379d593ca..f8a666c97e 100644 --- a/docs/development/saml/README.md +++ b/docs/development/saml/README.md @@ -111,27 +111,28 @@ On the OpenProject side, you'll have to configure SAML to connect to the just st -Here's a minimal configuration that you can put into `config/plugins/auth_saml/settings.yml` +Here's a minimal configuration that you can put into `config/configuration.yml` ```yaml -saml: - name: "saml" - display_name: "simplesaml-docker" - # Use the default SAML icon - icon: "auth_provider-saml.png" - # omniauth-saml config - assertion_consumer_service_url: "http://localhost:3000/auth/saml/callback" - issuer: "http://localhost:3000" - idp_cert_fingerprint: "119b9e027959cdb7c662cfd075d9e2ef384e445f" - idp_sso_target_url: "http://localhost:8080/simplesaml/saml2/idp/SSOService.php" - idp_slo_target_url: "http://localhost:8080/simplesaml/saml2/idp/SingleLogoutService.php" - attribute_statements: - email: ['email'] - login: ['uid'] - first_name: ['givenName'] - last_name: ['sn'] +default: + saml: + name: "saml" + display_name: "simplesaml-docker" + # Use the default SAML icon + icon: "auth_provider-saml.png" + # omniauth-saml config + assertion_consumer_service_url: "http://localhost:3000/auth/saml/callback" + issuer: "http://localhost:3000" + idp_cert_fingerprint: "119b9e027959cdb7c662cfd075d9e2ef384e445f" + idp_sso_target_url: "http://localhost:8080/simplesaml/saml2/idp/SSOService.php" + idp_slo_target_url: "http://localhost:8080/simplesaml/saml2/idp/SingleLogoutService.php" + attribute_statements: + email: ['email'] + login: ['uid'] + first_name: ['givenName'] + last_name: ['sn'] ``` diff --git a/docs/system-admin-guide/authentication/saml/README.md b/docs/system-admin-guide/authentication/saml/README.md index 9698e5a58b..1938561c27 100644 --- a/docs/system-admin-guide/authentication/saml/README.md +++ b/docs/system-admin-guide/authentication/saml/README.md @@ -19,7 +19,7 @@ You can integrate your active directory or other SAML compliant identity provide The configuration can be provided in one of two ways: -* `config/plugins/auth_saml/settings.yml` file (1.1) +* `config/configuration.yml` file (1.1) * Environment variables (1.2) @@ -30,23 +30,19 @@ The configuration can be provided in one of two ways: Whatever means are chosen, the plugin simply passes all options to omniauth-saml. See [their configuration documentation](https://github.com/omniauth/omniauth-saml#usage) for further details. -The options are mutually exclusive. I.e. if settings are already provided via ENV variables, settings in a `settings.yml` file will be ignored. If you decide to save settings in the database, they will override any ENV variables you might have set. +The options are mutually exclusive. I.e. if settings are already provided via ENV variables, they will overwrite settings in a `configuration.yml` file. +If you decide to save settings in the database, they will override any ENV variables you might have set. -#### 1.1 config/plugins/auth_saml/settings.yml file +#### 1.1 config/configuration.yml file -You need to create the folder `plugins` and `auth_saml` first. You can do that with the following command +In your OpenProject packaged installation, you can modify the `/opt/openproject/config/configuration.yml` file. +Edit the file in your favorite editor ``` -mkdir -p /opt/openproject/config/plugins/auth_saml +vim /opt/openproject/config/configuration.yml ``` -and then edit the file in your favorite editor - -``` -vim /opt/openproject/config/plugins/auth_saml/settings.yml -``` - -In your OpenProject packaged installation, you can modify the `/opt/openproject/config/plugins/auth_saml/settings.yml` file. This will contains the complete OpenProject configuration and can be extended to also contain metadata settings and connection details for your SSO identity provider. +This will contains the complete OpenProject configuration and can be extended to also contain metadata settings and connection details for your SSO identity provider. The following is an exemplary file with a set of common settings: @@ -87,7 +83,7 @@ saml: last_name: ['sn'] ``` -Be sure to choose the correct indentation and base key. The items below the `saml` key should be indented two spaces. You will get an YAML parsing error otherwise when trying to start OpenProject. +Be sure to choose the correct indentation and base key. The items below the `saml` key should be indented two spaces more than `saml` already is. And `saml` can will need to be placed in the `default` or `production` group so it will already be indented. You will get an YAML parsing error otherwise when trying to start OpenProject. #### 1.2 Environment variables @@ -122,7 +118,7 @@ That means it's best to set them using the console. > docker-compose run --rm web bundle exec rails console ``` -Once on the console you can set the same values as named in the `settings.yml` file, however they need to be nested within a 'providers' key as follows. +Once on the console you can set the same values as named in the `configuration.yml` file, however they need to be nested within a 'providers' key as follows. For example: ```ruby From 056fce4b1921d637dcb00fa9c1314c1cd9c668f8 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 1 Apr 2022 14:56:44 +0200 Subject: [PATCH 30/60] improve error message on unwritable setting assignment --- app/models/setting.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/setting.rb b/app/models/setting.rb index 384aec6063..bf013df9e5 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -157,7 +157,9 @@ class Setting < ApplicationRecord end def value=(val) - raise NoMethodError unless Settings::Definition[name].writable? + unless Settings::Definition[name].writable? + raise NoMethodError, "#{name} is not writable but can be set through env vars or configuration.yml file." + end write_attribute(:value, formatted_value(val)) end From c84aa3de63a2f03f914eba5a8de3f566c476e936 Mon Sep 17 00:00:00 2001 From: OpenProject Actions CI Date: Sat, 2 Apr 2022 03:14:35 +0000 Subject: [PATCH 31/60] update locales from crowdin [ci skip] --- config/locales/crowdin/js-ru.yml | 4 ++-- modules/team_planner/config/locales/crowdin/js-ru.yml | 2 +- .../config/locales/crowdin/zh-TW.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/locales/crowdin/js-ru.yml b/config/locales/crowdin/js-ru.yml index bfbaa600c2..7578f884de 100644 --- a/config/locales/crowdin/js-ru.yml +++ b/config/locales/crowdin/js-ru.yml @@ -997,7 +997,7 @@ ru: relations: Связи watchers: Наблюдатель files: Файлы - files_tab_migration_help: 'You can now attach files to work packages via the new tab:' + files_tab_migration_help: 'Теперь вы можете прикрепить файлы к рабочим пакетам через новую вкладку:' time_relative: days: "дней" weeks: "недели" @@ -1171,7 +1171,7 @@ ru: group: 'Теперь группа является частью %{project}. Тем временем вы уже можете спланировать работу с этой группой и назначить пакеты работ.' next_button: 'Продолжить' include_projects: - toggle_title: 'Include projects' + toggle_title: 'Включить проекты' title: 'Проекты' clear_selection: 'Отменить выделение' apply: 'Применить' diff --git a/modules/team_planner/config/locales/crowdin/js-ru.yml b/modules/team_planner/config/locales/crowdin/js-ru.yml index 869ebcd274..a5e5daf038 100644 --- a/modules/team_planner/config/locales/crowdin/js-ru.yml +++ b/modules/team_planner/config/locales/crowdin/js-ru.yml @@ -2,7 +2,7 @@ ru: js: team_planner: - add_existing: 'Add existing' + add_existing: 'Добавить существующее' create_new: 'Новый планировщик для команды' title: 'Командный планировщик' unsaved_title: 'Безымянный планировщик команды' diff --git a/modules/two_factor_authentication/config/locales/crowdin/zh-TW.yml b/modules/two_factor_authentication/config/locales/crowdin/zh-TW.yml index 21c4e4439c..f44deb437e 100644 --- a/modules/two_factor_authentication/config/locales/crowdin/zh-TW.yml +++ b/modules/two_factor_authentication/config/locales/crowdin/zh-TW.yml @@ -71,7 +71,7 @@ zh-TW: text_2fa_enabled: '每次登錄時, 都會要求該使用者從預設的2FA 設備中取得 OTP 密碼。' text_2fa_disabled: "使用者沒有透過【我的帳戶】頁面進行建立一個2FA設備的動作。" upsale: - title: 'Two-factor authentication' + title: '雙重身分驗證' description: '請使用第二種的認證方式加強您內部或外部的認證機制' backup_codes: none_found: 這個帳戶沒有存在備份代碼 @@ -165,7 +165,7 @@ zh-TW: label_confirmed: '已確認' button_continue: '繼續' button_make_default: '標記為預設值' - label_unverified_phone: "Cell phone not yet verified" + label_unverified_phone: "手機尚未驗證" notice_phone_number_format: "請輸入以下格式的號碼: + XX XXXXXXXX。" text_otp_not_receive: "其他驗證方法" text_send_otp_again: "通過以下操作,重新發送一次性密碼:" From 52c98687adbe78a8088fea574a2fc88c56a7d303 Mon Sep 17 00:00:00 2001 From: OpenProject Actions CI Date: Mon, 4 Apr 2022 03:19:39 +0000 Subject: [PATCH 32/60] update locales from crowdin [ci skip] --- config/locales/crowdin/js-lt.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/crowdin/js-lt.yml b/config/locales/crowdin/js-lt.yml index 626d6cc1e1..e05bc9ddfb 100644 --- a/config/locales/crowdin/js-lt.yml +++ b/config/locales/crowdin/js-lt.yml @@ -998,7 +998,7 @@ lt: relations: Ryšiai watchers: Stebėtojai files: Failai - files_tab_migration_help: 'You can now attach files to work packages via the new tab:' + files_tab_migration_help: 'Dabar jūs galite prisegti failus prie darbo paketųnaujoje kortelėje:' time_relative: days: "dienos" weeks: "savaitės" From 3b0b0a93b2357e20127228a8e9504960c93fa815 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 1 Apr 2022 16:32:14 +0200 Subject: [PATCH 33/60] join status explicitly triggered by status filter --- app/models/queries/work_packages/filter/status_filter.rb | 4 ++++ app/models/query/results.rb | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/models/queries/work_packages/filter/status_filter.rb b/app/models/queries/work_packages/filter/status_filter.rb index fc6857a077..765c9db8fb 100644 --- a/app/models/queries/work_packages/filter/status_filter.rb +++ b/app/models/queries/work_packages/filter/status_filter.rb @@ -65,6 +65,10 @@ class Queries::WorkPackages::Filter::StatusFilter < Queries::WorkPackages::Filte true end + def joins + :status + end + private def all_statuses diff --git a/app/models/query/results.rb b/app/models/query/results.rb index ebbabd5f4f..33d2db44fd 100644 --- a/app/models/query/results.rb +++ b/app/models/query/results.rb @@ -41,9 +41,9 @@ class ::Query::Results def work_package_count work_package_scope .joins(all_filter_joins) - .includes(:status, :project) + .includes(:project) .where(query.statement) - .references(:statuses, :projects) + .references(:projects) .count rescue ::ActiveRecord::StatementInvalid => e raise ::Query::StatementInvalid.new(e.message) @@ -79,7 +79,7 @@ class ::Query::Results end def all_includes - (%i(status project) + + (%i(project) + includes_for_columns(include_columns)).uniq end From 98f451c302793859e0125075c2e1c62f1eb634d9 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 4 Apr 2022 07:21:01 +0200 Subject: [PATCH 34/60] Feature/40215 restructure email settings (#10415) * rename user admin menu item * rename email admin menu item * restructure api and email admin menus --- ....rb => aggregation_settings_controller.rb} | 4 +- .../admin/settings/api_settings_controller.rb | 2 +- app/helpers/settings_helper.rb | 5 --- app/views/admin/index.html.erb | 2 +- .../show.html.erb | 12 +++-- .../admin/settings/api_settings/show.html.erb | 2 +- config/initializers/menus.rb | 28 ++++++++---- config/locales/en.yml | 11 +++-- config/routes.rb | 3 +- docs/system-admin-guide/README.md | 44 +++++++++---------- .../incoming-and-outgoing/README.md | 26 +++++------ .../users-permissions/README.md | 4 +- .../users-permissions/avatars/README.md | 2 +- .../users-permissions/groups/README.md | 2 +- .../placeholder-users/README.md | 2 +- .../roles-permissions/README.md | 2 +- .../users-permissions-faq/README.md | 4 +- .../users-permissions/users/README.md | 2 +- .../time-and-costs/time-tracking/README.md | 2 +- .../webhooks/outgoing/admin/index.html.erb | 5 +++ modules/webhooks/config/locales/en.yml | 5 +++ modules/webhooks/config/routes.rb | 10 +++-- .../lib/open_project/webhooks/engine.rb | 10 +---- .../outgoing/admin_controller_spec.rb | 28 ++++++------ spec/features/users/edit_users_spec.rb | 2 +- 25 files changed, 121 insertions(+), 98 deletions(-) rename app/controllers/admin/settings/{notifications_settings_controller.rb => aggregation_settings_controller.rb} (92%) rename app/views/admin/settings/{notifications_settings => aggregation_settings}/show.html.erb (78%) diff --git a/app/controllers/admin/settings/notifications_settings_controller.rb b/app/controllers/admin/settings/aggregation_settings_controller.rb similarity index 92% rename from app/controllers/admin/settings/notifications_settings_controller.rb rename to app/controllers/admin/settings/aggregation_settings_controller.rb index 694742c54a..fb87cf4a01 100644 --- a/app/controllers/admin/settings/notifications_settings_controller.rb +++ b/app/controllers/admin/settings/aggregation_settings_controller.rb @@ -27,7 +27,7 @@ #++ module Admin::Settings - class NotificationsSettingsController < ::Admin::SettingsController + class AggregationSettingsController < ::Admin::SettingsController current_menu_item [:show] do :notification_settings end @@ -37,7 +37,7 @@ module Admin::Settings end def default_breadcrumb - t(:'menus.admin.incoming_outgoing') + t(:'menus.admin.aggregation_and_retention') end def show_local_breadcrumb diff --git a/app/controllers/admin/settings/api_settings_controller.rb b/app/controllers/admin/settings/api_settings_controller.rb index ab4371803b..e7233cbe0a 100644 --- a/app/controllers/admin/settings/api_settings_controller.rb +++ b/app/controllers/admin/settings/api_settings_controller.rb @@ -28,7 +28,7 @@ module Admin::Settings class APISettingsController < ::Admin::SettingsController - menu_item :settings_api + menu_item :api def default_breadcrumb t(:label_api_access_key_type) diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 10417dd1da..acf0a3dd33 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -54,11 +54,6 @@ module SettingsHelper controller: '/admin/settings/attachments_settings', label: :'attributes.attachments' }, - { - name: 'api', - controller: '/admin/settings/api_settings', - label: :label_api_access_key_type - }, { name: 'repositories', controller:'/admin/settings/repositories_settings', diff --git a/app/views/admin/index.html.erb b/app/views/admin/index.html.erb index 5eec47a1ab..6e5e694aa6 100644 --- a/app/views/admin/index.html.erb +++ b/app/views/admin/index.html.erb @@ -34,7 +34,7 @@ See COPYRIGHT and LICENSE files for more details.
        • <% end %> +

          + <%= t('webhooks.outgoing.explanation.text', + link: link_to(t(:'webhooks.outgoing.explanation.link'), admin_settings_aggregation_path, target: '_blank')).html_safe %> +

          + <%= rails_cell ::Webhooks::Outgoing::Webhooks::TableCell, @webhooks %> diff --git a/modules/webhooks/config/locales/en.yml b/modules/webhooks/config/locales/en.yml index 0df7114387..cf795fc926 100644 --- a/modules/webhooks/config/locales/en.yml +++ b/modules/webhooks/config/locales/en.yml @@ -27,6 +27,11 @@ en: events: created: "Created" updated: "Updated" + explanation: + text: > + Upon the occurrence of an event like the creation of a work package or an update on a project, OpenProject will send a POST request to the configured web endpoints. + Oftentimes, the event is sent after the %{link} has passed. + link: configured aggregation period status: enabled: 'Webhook is enabled' disabled: 'Webhook is disabled' diff --git a/modules/webhooks/config/routes.rb b/modules/webhooks/config/routes.rb index 500cf86bcd..eebea808f6 100644 --- a/modules/webhooks/config/routes.rb +++ b/modules/webhooks/config/routes.rb @@ -32,9 +32,11 @@ OpenProject::Application.routes.draw do end scope 'admin' do - resources :webhooks, - param: :webhook_id, - controller: 'webhooks/outgoing/admin', - as: 'admin_outgoing_webhooks' + scope :settings do + resources :webhooks, + param: :webhook_id, + controller: 'webhooks/outgoing/admin', + as: 'admin_outgoing_webhooks' + end end end diff --git a/modules/webhooks/lib/open_project/webhooks/engine.rb b/modules/webhooks/lib/open_project/webhooks/engine.rb index 2a38fc73b4..5cce8e5b45 100644 --- a/modules/webhooks/lib/open_project/webhooks/engine.rb +++ b/modules/webhooks/lib/open_project/webhooks/engine.rb @@ -41,14 +41,8 @@ module OpenProject::Webhooks :plugin_webhooks, { controller: 'webhooks/outgoing/admin', action: :index }, if: Proc.new { User.current.admin? }, - parent: :in_out, - caption: ->(*) { I18n.t('webhooks.plural') } - end - - config.before_configuration do |app| - # This is required for the routes to be loaded first as the routes should - # be prepended so they take precedence over the core. - app.config.paths['config/routes.rb'].unshift File.join(File.dirname(__FILE__), "..", "..", "..", "config", "routes.rb") + parent: :api_and_webhooks, + caption: :'webhooks.plural' end initializer 'webhooks.subscribe_to_notifications' do diff --git a/modules/webhooks/spec/routing/webhooks/outgoing/admin_controller_spec.rb b/modules/webhooks/spec/routing/webhooks/outgoing/admin_controller_spec.rb index 87f1e8117b..be63a153a0 100644 --- a/modules/webhooks/spec/routing/webhooks/outgoing/admin_controller_spec.rb +++ b/modules/webhooks/spec/routing/webhooks/outgoing/admin_controller_spec.rb @@ -30,34 +30,34 @@ require 'spec_helper' describe 'Outgoing webhooks administration', type: :routing do it 'route to index' do - expect(get('/admin/webhooks')).to route_to('webhooks/outgoing/admin#index') + expect(get('/admin/settings/webhooks')).to route_to('webhooks/outgoing/admin#index') end it 'route to new' do - expect(get('/admin/webhooks/new')).to route_to('webhooks/outgoing/admin#new') + expect(get('/admin/settings/webhooks/new')).to route_to('webhooks/outgoing/admin#new') end it 'route to show' do - expect(get('/admin/webhooks/1')).to route_to(controller: 'webhooks/outgoing/admin', - action: 'show', - webhook_id: '1') + expect(get('/admin/settings/webhooks/1')).to route_to(controller: 'webhooks/outgoing/admin', + action: 'show', + webhook_id: '1') end it 'route to edit' do - expect(get('/admin/webhooks/1/edit')).to route_to(controller: 'webhooks/outgoing/admin', - action: 'edit', - webhook_id: '1') + expect(get('/admin/settings/webhooks/1/edit')).to route_to(controller: 'webhooks/outgoing/admin', + action: 'edit', + webhook_id: '1') end it 'route to PUT update' do - expect(put('/admin/webhooks/1')).to route_to(controller: 'webhooks/outgoing/admin', - action: 'update', - webhook_id: '1') + expect(put('/admin/settings/webhooks/1')).to route_to(controller: 'webhooks/outgoing/admin', + action: 'update', + webhook_id: '1') end it 'route to DELETE destroy' do - expect(delete('/admin/webhooks/1')).to route_to(controller: 'webhooks/outgoing/admin', - action: 'destroy', - webhook_id: '1') + expect(delete('/admin/settings/webhooks/1')).to route_to(controller: 'webhooks/outgoing/admin', + action: 'destroy', + webhook_id: '1') end end diff --git a/spec/features/users/edit_users_spec.rb b/spec/features/users/edit_users_spec.rb index 1d4e78f145..4ef2d8850e 100644 --- a/spec/features/users/edit_users_spec.rb +++ b/spec/features/users/edit_users_spec.rb @@ -92,7 +92,7 @@ describe 'edit users', type: :feature, js: true do visit edit_user_path(user) expect(page).to have_no_selector('.admin-overview-menu-item', text: 'Overview') - expect(page).to have_no_selector('.users-and-permissions-menu-item', text: 'Users & Permissions') + expect(page).to have_no_selector('.users-and-permissions-menu-item', text: 'Users and permissions') expect(page).to have_selector('.users-menu-item.selected', text: 'Users') expect(page).to have_selector 'select#user_auth_source_id' From 5b786c2365c660215184f7ae58b1447a8d84bec2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 05:32:18 +0000 Subject: [PATCH 35/60] Bump html-pipeline from 2.14.0 to 2.14.1 Bumps [html-pipeline](https://github.com/jch/html-pipeline) from 2.14.0 to 2.14.1. - [Release notes](https://github.com/jch/html-pipeline/releases) - [Changelog](https://github.com/gjtorikian/html-pipeline/blob/main/CHANGELOG.md) - [Commits](https://github.com/jch/html-pipeline/compare/v2.14.0...v2.14.1) --- updated-dependencies: - dependency-name: html-pipeline dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d915b8b6e3..d6c53f059d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -537,7 +537,7 @@ GEM hashdiff (1.0.1) hashery (2.1.2) hashie (3.6.0) - html-pipeline (2.14.0) + html-pipeline (2.14.1) activesupport (>= 2) nokogiri (>= 1.4) htmldiff (0.0.1) From 00c6fd2692780b32a0c071ceb550eb2acbf2948a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 23 Mar 2022 09:49:41 +0100 Subject: [PATCH 36/60] Add new query attribute include_subprojects --- .../work_packages/filter/project_filter.rb | 15 ++++++++++++++- app/models/query.rb | 1 + ...0323083000_add_include_subprojects_to_query.rb | 14 ++++++++++++++ lib/api/v3/queries/query_representer.rb | 2 ++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20220323083000_add_include_subprojects_to_query.rb diff --git a/app/models/queries/work_packages/filter/project_filter.rb b/app/models/queries/work_packages/filter/project_filter.rb index 9f81d54458..11866a7abc 100644 --- a/app/models/queries/work_packages/filter/project_filter.rb +++ b/app/models/queries/work_packages/filter/project_filter.rb @@ -59,7 +59,7 @@ class Queries::WorkPackages::Filter::ProjectFilter < Queries::WorkPackages::Filt available_projects = visible_projects.index_by(&:id) values - .map { |project_id| available_projects[project_id.to_i] } + .flat_map { |project_id| expanded_subprojects(available_projects[project_id.to_i]) } .compact end @@ -68,4 +68,17 @@ class Queries::WorkPackages::Filter::ProjectFilter < Queries::WorkPackages::Filt def visible_projects @visible_projects ||= Project.visible.active end + + ## + # Depending on whether subprojects are included in the query, + # expand selected projects with its descendants + def expanded_subprojects(selected_project) + return if selected_project.nil? + + if context.include_subprojects? + [selected_project] + else + [selected_project].concat(selected_project.descendants.visible) + end + end end diff --git a/app/models/query.rb b/app/models/query.rb index e14a6f5a5a..9fc48f8938 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -62,6 +62,7 @@ class Query < ApplicationRecord query.add_default_filter query.set_default_sort query.show_hierarchies = true + query.include_subprojects = Setting.display_subprojects_work_packages? end end diff --git a/db/migrate/20220323083000_add_include_subprojects_to_query.rb b/db/migrate/20220323083000_add_include_subprojects_to_query.rb new file mode 100644 index 0000000000..eac71f74d7 --- /dev/null +++ b/db/migrate/20220323083000_add_include_subprojects_to_query.rb @@ -0,0 +1,14 @@ +class AddIncludeSubprojectsToQuery < ActiveRecord::Migration[6.1] + def change + add_column :queries, + :include_subprojects, + :boolean, + null: false, + default: Setting.display_subprojects_work_packages? + + # Remove the default now + reversible do |dir| + dir.up { change_column_default :queries, :include_subprojects, nil } + end + end +end diff --git a/lib/api/v3/queries/query_representer.rb b/lib/api/v3/queries/query_representer.rb index 2543999998..95def2a591 100644 --- a/lib/api/v3/queries/query_representer.rb +++ b/lib/api/v3/queries/query_representer.rb @@ -288,6 +288,8 @@ module API property :filters, exec_context: :decorator + property :include_subprojects + property :display_sums, as: :sums property :public From 7673d3951514bc0d2db4d10ce462c20aed1812c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 28 Mar 2022 08:56:54 +0200 Subject: [PATCH 37/60] Set include_subprojects in factories --- spec/factories/query_factory.rb | 1 + spec_legacy/fixtures/queries.yml | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/spec/factories/query_factory.rb b/spec/factories/query_factory.rb index 35b81959f1..e2ad644639 100644 --- a/spec/factories/query_factory.rb +++ b/spec/factories/query_factory.rb @@ -30,6 +30,7 @@ FactoryBot.define do factory :query do project user factory: :user + include_subprojects { true } sequence(:name) { |n| "Query #{n}" } factory :public_query do diff --git a/spec_legacy/fixtures/queries.yml b/spec_legacy/fixtures/queries.yml index 465e506de7..8abf181191 100644 --- a/spec_legacy/fixtures/queries.yml +++ b/spec_legacy/fixtures/queries.yml @@ -31,6 +31,7 @@ queries_001: id: 1 project_id: 1 public: true + include_subprojects: true name: Multiple custom fields query filters: | --- @@ -53,6 +54,7 @@ queries_002: id: 2 project_id: 1 public: false + include_subprojects: true name: Private query for cookbook filters: | --- @@ -71,6 +73,7 @@ queries_003: id: 3 project_id: public: false + include_subprojects: true name: Private query for all projects filters: | --- @@ -85,6 +88,7 @@ queries_004: id: 4 project_id: public: true + include_subprojects: true name: Public query for all projects filters: | --- @@ -99,6 +103,7 @@ queries_005: id: 5 project_id: public: true + include_subprojects: true name: Open issues by priority and type filters: | --- @@ -119,6 +124,7 @@ queries_006: id: 6 project_id: public: true + include_subprojects: true name: Open issues grouped by type filters: | --- @@ -138,6 +144,7 @@ queries_007: id: 7 project_id: 2 public: true + include_subprojects: true name: Public query for project 2 filters: | --- @@ -152,6 +159,7 @@ queries_008: id: 8 project_id: 2 public: false + include_subprojects: true name: Private query for project 2 filters: | --- @@ -166,6 +174,7 @@ queries_009: id: 9 project_id: public: true + include_subprojects: true name: Open issues grouped by list custom field filters: | --- From 92430943475b775f80d69d805ee1116e1a0dd0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 28 Mar 2022 08:57:04 +0200 Subject: [PATCH 38/60] Replace create_query helper with default endpoint --- app/contracts/queries/base_contract.rb | 1 + app/services/base_services/set_attributes.rb | 2 ++ app/services/queries/create_service.rb | 23 ++++--------------- .../queries/set_attributes_service.rb | 16 ++++++++++++- lib/api/v3/queries/queries_api.rb | 6 ++--- lib/api/v3/queries/query_helper.rb | 11 --------- 6 files changed, 25 insertions(+), 34 deletions(-) diff --git a/app/contracts/queries/base_contract.rb b/app/contracts/queries/base_contract.rb index e2ed078289..813a40c2d9 100644 --- a/app/contracts/queries/base_contract.rb +++ b/app/contracts/queries/base_contract.rb @@ -43,6 +43,7 @@ module Queries attribute :highlighted_attributes attribute :show_hierarchies attribute :display_representation + attribute :include_subprojects attribute :column_names # => columns attribute :filters diff --git a/app/services/base_services/set_attributes.rb b/app/services/base_services/set_attributes.rb index 46109999d2..7fb2618cc0 100644 --- a/app/services/base_services/set_attributes.rb +++ b/app/services/base_services/set_attributes.rb @@ -31,6 +31,8 @@ module BaseServices include Contracted def initialize(user:, model:, contract_class:, contract_options: {}) + super() + self.user = user self.model = prepare_model(model) diff --git a/app/services/queries/create_service.rb b/app/services/queries/create_service.rb index 3ae93b737b..f5dbc9c9a9 100644 --- a/app/services/queries/create_service.rb +++ b/app/services/queries/create_service.rb @@ -26,20 +26,9 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Queries::CreateService < Queries::BaseService - def initialize(**args) - super(**args) - self.contract_class = Queries::CreateContract - end - - def call(query) - remove_invalid_order(query) - super - end - - private - - def remove_invalid_order(query) +class Queries::CreateService < ::BaseServices::Create + def after_validate(params, call) + query = call.result # Check which of the work package IDs exist ids = query.ordered_work_packages.map(&:work_package_id) existent_wps = WorkPackage.where(id: ids).pluck(:id).to_set @@ -47,11 +36,7 @@ class Queries::CreateService < Queries::BaseService query.ordered_work_packages = query.ordered_work_packages.select do |order_item| existent_wps.include?(order_item.work_package_id) end - end - - def service_result(result, errors, query) - query.update user: user - super + call end end diff --git a/app/services/queries/set_attributes_service.rb b/app/services/queries/set_attributes_service.rb index b3ae12f922..6488d5aed9 100644 --- a/app/services/queries/set_attributes_service.rb +++ b/app/services/queries/set_attributes_service.rb @@ -26,4 +26,18 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Queries::SetAttributesService < ::BaseServices::SetAttributes; end +class Queries::SetAttributesService < ::BaseServices::SetAttributes + def set_default_attributes(_params) + if model.include_subprojects.nil? + model.include_subprojects = Setting.display_subprojects_work_packages? + end + + set_default_user + end + + def set_default_user + model.change_by_system do + model.user = user + end + end +end diff --git a/lib/api/v3/queries/queries_api.rb b/lib/api/v3/queries/queries_api.rb index 7076eac455..f9be692b42 100644 --- a/lib/api/v3/queries/queries_api.rb +++ b/lib/api/v3/queries/queries_api.rb @@ -96,9 +96,9 @@ module API end end - post do - create_query request_body, current_user - end + post &::API::V3::Utilities::Endpoints::Create + .new(model: Query) + .mount route_param :id, type: Integer, desc: 'Query ID' do after_validation do diff --git a/lib/api/v3/queries/query_helper.rb b/lib/api/v3/queries/query_helper.rb index 9d511eff04..f9cc83c776 100644 --- a/lib/api/v3/queries/query_helper.rb +++ b/lib/api/v3/queries/query_helper.rb @@ -63,17 +63,6 @@ module API end end - def create_query(request_body, current_user) - rep = representer.new Query.new, current_user: current_user - query = rep.from_hash request_body - call = ::Queries::CreateService.new(user: current_user).call query - if call.success? - representer.new call.result, current_user: current_user, embed_links: true - else - fail ::API::Errors::ErrorBase.create_and_merge_errors(call.errors) - end - end - def update_query(query, request_body, current_user) rep = representer.new query, current_user: current_user query = rep.from_hash request_body From ad9e4546b52509975862f6663ad37a98730123a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 28 Mar 2022 09:16:21 +0200 Subject: [PATCH 39/60] Adapt spec to new query create service --- app/services/queries/create_service.rb | 2 +- spec/services/queries/create_service_spec.rb | 15 +++++++++------ .../work_packages/table_configuration_modal.rb | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/services/queries/create_service.rb b/app/services/queries/create_service.rb index f5dbc9c9a9..3f0ffd5215 100644 --- a/app/services/queries/create_service.rb +++ b/app/services/queries/create_service.rb @@ -27,7 +27,7 @@ #++ class Queries::CreateService < ::BaseServices::Create - def after_validate(params, call) + def after_validate(_params, call) query = call.result # Check which of the work package IDs exist ids = query.ordered_work_packages.map(&:work_package_id) diff --git a/spec/services/queries/create_service_spec.rb b/spec/services/queries/create_service_spec.rb index 328b0c3e8b..58370d5033 100644 --- a/spec/services/queries/create_service_spec.rb +++ b/spec/services/queries/create_service_spec.rb @@ -30,17 +30,20 @@ require 'spec_helper' describe Queries::CreateService do let(:user) { build_stubbed(:admin) } - let(:query) { build(:query, user: user) } let(:instance) { described_class.new(user: user) } - subject { instance.call(query).result } + subject { instance.call(params).result } describe 'ordered work packages' do let!(:work_package) { create :work_package } - - before do - query.ordered_work_packages.build(work_package_id: work_package.id, position: 0) - query.ordered_work_packages.build(work_package_id: 99999, position: 1) + let(:params) do + { + name: 'My query', + ordered_work_packages: { + work_package.id => 0, + 9999 => 1 + } + } end it 'removes items for which work packages do not exist' do diff --git a/spec/support/components/work_packages/table_configuration_modal.rb b/spec/support/components/work_packages/table_configuration_modal.rb index 01880f9c75..a449160fed 100644 --- a/spec/support/components/work_packages/table_configuration_modal.rb +++ b/spec/support/components/work_packages/table_configuration_modal.rb @@ -89,7 +89,7 @@ module Components end def expect_disabled_tab(name) - expect(page).to have_selector("#{selector} [data-qa-tab-disabled]", text: name.upcase) + expect(page).to have_selector("#{selector} [data-qa-tab-disabled]", text: name.upcase, wait: 10) end def selected_tab(name) From e5b05b9fa0cfaeb41da6d4495f3dfea9dfdb5a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 28 Mar 2022 09:45:55 +0200 Subject: [PATCH 40/60] Add spec for changed subproject behavior --- app/models/query.rb | 2 +- spec/factories/query_factory.rb | 2 +- ...ults_subproject_filter_integration_spec.rb | 106 +++++++++++++++--- 3 files changed, 94 insertions(+), 16 deletions(-) diff --git a/app/models/query.rb b/app/models/query.rb index 9fc48f8938..82e91ccb6d 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -369,7 +369,7 @@ class Query < ApplicationRecord subproject_filter = Queries::WorkPackages::Filter::SubprojectFilter.create! subproject_filter.context = self - subproject_filter.operator = if Setting.display_subprojects_work_packages? + subproject_filter.operator = if include_subprojects? '*' else '!*' diff --git a/spec/factories/query_factory.rb b/spec/factories/query_factory.rb index e2ad644639..6d44755050 100644 --- a/spec/factories/query_factory.rb +++ b/spec/factories/query_factory.rb @@ -30,7 +30,7 @@ FactoryBot.define do factory :query do project user factory: :user - include_subprojects { true } + include_subprojects { Setting.display_subprojects_work_packages? } sequence(:name) { |n| "Query #{n}" } factory :public_query do diff --git a/spec/models/query/results_subproject_filter_integration_spec.rb b/spec/models/query/results_subproject_filter_integration_spec.rb index 52adc6bb80..5b9330e052 100644 --- a/spec/models/query/results_subproject_filter_integration_spec.rb +++ b/spec/models/query/results_subproject_filter_integration_spec.rb @@ -58,34 +58,112 @@ describe ::Query::Results, 'Subproject filter integration', type: :model, with_m login_as user end - context 'when subprojects included', with_settings: { display_subprojects_work_packages: true } do - it 'shows the sub work packages' do - expect(query_results.work_packages).to match_array [parent_wp, child_wp] + describe 'new default query' do + context 'when subprojects included', with_settings: { display_subprojects_work_packages: true } do + it 'shows the sub work packages' do + expect(query_results.work_packages).to match_array [parent_wp, child_wp] + end + end + + context 'when subprojects not included', with_settings: { display_subprojects_work_packages: false } do + it 'does not show the sub work packages' do + expect(query_results.work_packages).to match_array [parent_wp] + end + + context 'when subproject filter added manually' do + before do + query.add_filter('subproject_id', '=', [child_project.id]) + end + + it 'shows the sub work packages' do + expect(query_results.work_packages).to match_array [parent_wp, child_wp] + end + end + + context 'when only subproject filter added manually' do + before do + query.add_filter('only_subproject_id', '=', [child_project.id]) + end + + it 'shows only the sub work packages' do + expect(query_results.work_packages).to match_array [child_wp] + end + end end end - context 'when subprojects not included', with_settings: { display_subprojects_work_packages: false } do - it 'does not show the sub work packages' do - expect(query_results.work_packages).to match_array [parent_wp] + describe 'query with overriden include_subprojects = true' do + before do + query.include_subprojects = true end - context 'when subproject filter added manually' do - before do - query.add_filter('subproject_id', '=', [child_project.id]) + context 'when subprojects included', with_settings: { display_subprojects_work_packages: true } do + it 'shows the sub work packages' do + expect(query_results.work_packages).to match_array [parent_wp, child_wp] end + end + context 'when subprojects not included', with_settings: { display_subprojects_work_packages: false } do it 'shows the sub work packages' do expect(query_results.work_packages).to match_array [parent_wp, child_wp] end + + context 'when subproject filter added manually' do + before do + query.add_filter('subproject_id', '=', [child_project.id]) + end + + it 'shows the sub work packages' do + expect(query_results.work_packages).to match_array [parent_wp, child_wp] + end + end + + context 'when only subproject filter added manually' do + before do + query.add_filter('only_subproject_id', '=', [child_project.id]) + end + + it 'shows only the sub work packages' do + expect(query_results.work_packages).to match_array [child_wp] + end + end + end + end + + describe 'query with overriden include_subprojects = false' do + before do + query.include_subprojects = false end - context 'when only subproject filter added manually' do - before do - query.add_filter('only_subproject_id', '=', [child_project.id]) + context 'when subprojects included', with_settings: { display_subprojects_work_packages: true } do + it 'does not show the sub work packages' do + expect(query_results.work_packages).to match_array [parent_wp] end + end + + context 'when subprojects not included', with_settings: { display_subprojects_work_packages: false } do + it 'does not show the sub work packages' do + expect(query_results.work_packages).to match_array [parent_wp] + end + + context 'when subproject filter added manually' do + before do + query.add_filter('subproject_id', '=', [child_project.id]) + end + + it 'shows the sub work packages' do + expect(query_results.work_packages).to match_array [parent_wp, child_wp] + end + end + + context 'when only subproject filter added manually' do + before do + query.add_filter('only_subproject_id', '=', [child_project.id]) + end - it 'shows only the sub work packages' do - expect(query_results.work_packages).to match_array [child_wp] + it 'shows only the sub work packages' do + expect(query_results.work_packages).to match_array [child_wp] + end end end end From df220ed594d713eaec09b1de9fe823e6efd1c5b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 28 Mar 2022 10:21:10 +0200 Subject: [PATCH 41/60] Extend project filter with replaced values and add spec --- .../work_packages/filter/project_filter.rb | 19 ++- app/seeders/demo_data/query_builder.rb | 3 +- .../demo_data/work_package_board_seeder.rb | 1 + lib/api/v3/queries/query_representer.rb | 6 +- .../features/calendar_project_include_spec.rb | 8 +- .../team_planner_project_include_spec.rb | 8 +- ...ork_packages_table_project_include_spec.rb | 14 +- .../timeline/timeline_labels_spec.rb | 6 +- .../queries/query_representer_parsing_spec.rb | 15 +- .../filter/project_filter_spec.rb | 20 ++- ...results_project_filter_integration_spec.rb | 150 ++++++++++++++++++ ...eferences_service_call_integration_spec.rb | 8 +- 12 files changed, 217 insertions(+), 41 deletions(-) create mode 100644 spec/models/query/results_project_filter_integration_spec.rb diff --git a/app/models/queries/work_packages/filter/project_filter.rb b/app/models/queries/work_packages/filter/project_filter.rb index 11866a7abc..dd7e88d9ee 100644 --- a/app/models/queries/work_packages/filter/project_filter.rb +++ b/app/models/queries/work_packages/filter/project_filter.rb @@ -59,8 +59,13 @@ class Queries::WorkPackages::Filter::ProjectFilter < Queries::WorkPackages::Filt available_projects = visible_projects.index_by(&:id) values - .flat_map { |project_id| expanded_subprojects(available_projects[project_id.to_i]) } + .flat_map { |project_id| available_projects[project_id.to_i] } .compact + .uniq + end + + def where + operator_strategy.sql_for_field(projects_and_descendants, self.class.model.table_name, :project_id) end private @@ -72,13 +77,17 @@ class Queries::WorkPackages::Filter::ProjectFilter < Queries::WorkPackages::Filt ## # Depending on whether subprojects are included in the query, # expand selected projects with its descendants - def expanded_subprojects(selected_project) - return if selected_project.nil? + def projects_and_descendants + value_objects + .inject(Set.new) { |project_set, project| project_set + expand_subprojects(project) } + .map(&:id) + end + def expand_subprojects(selected_project) if context.include_subprojects? - [selected_project] - else [selected_project].concat(selected_project.descendants.visible) + else + [selected_project] end end end diff --git a/app/seeders/demo_data/query_builder.rb b/app/seeders/demo_data/query_builder.rb index 17ba39c7f0..d8832d8bbc 100644 --- a/app/seeders/demo_data/query_builder.rb +++ b/app/seeders/demo_data/query_builder.rb @@ -52,7 +52,8 @@ module DemoData public: config.fetch(:public, true), starred: config.fetch(:starred, false), show_hierarchies: config.fetch(:hierarchy, false), - timeline_visible: config.fetch(:timeline, false) + timeline_visible: config.fetch(:timeline, false), + include_subprojects: true } end diff --git a/app/seeders/demo_data/work_package_board_seeder.rb b/app/seeders/demo_data/work_package_board_seeder.rb index 418f8f2533..d950663713 100644 --- a/app/seeders/demo_data/work_package_board_seeder.rb +++ b/app/seeders/demo_data/work_package_board_seeder.rb @@ -160,6 +160,7 @@ module DemoData Query.new(project: project, user: admin).tap do |query| # Make it public so that new members can see it too query.public = true + query.include_subprojects = true query.name = list[:name] diff --git a/lib/api/v3/queries/query_representer.rb b/lib/api/v3/queries/query_representer.rb index 95def2a591..af6f87be52 100644 --- a/lib/api/v3/queries/query_representer.rb +++ b/lib/api/v3/queries/query_representer.rb @@ -259,11 +259,9 @@ module API exec_context: :decorator, getter: nil, setter: ->(fragment:, **) { - next unless represented.new_record? + next if represented.persisted? - Hash(fragment).each do |wp_id, position| - represented.ordered_work_packages.build(work_package_id: wp_id, position: position) - end + represented.ordered_work_packages = fragment } property :starred, diff --git a/modules/calendar/spec/features/calendar_project_include_spec.rb b/modules/calendar/spec/features/calendar_project_include_spec.rb index 5919e63273..c1c36dd0db 100644 --- a/modules/calendar/spec/features/calendar_project_include_spec.rb +++ b/modules/calendar/spec/features/calendar_project_include_spec.rb @@ -50,8 +50,8 @@ describe 'Calendar project include', type: :feature, js: true do dropdown.expect_closed work_package_view.expect_event task - work_package_view.expect_event sub_bug, present: false - work_package_view.expect_event sub_sub_bug, present: false + work_package_view.expect_event sub_bug, present: true + work_package_view.expect_event sub_sub_bug, present: true work_package_view.expect_event other_task work_package_view.expect_event other_other_task, present: false @@ -60,7 +60,7 @@ describe 'Calendar project include', type: :feature, js: true do dropdown.click_button 'Apply' dropdown.expect_count 2 - work_package_view.expect_event sub_bug, present: false + work_package_view.expect_event sub_bug, present: true work_package_view.expect_event sub_sub_bug dropdown.toggle! @@ -74,7 +74,7 @@ describe 'Calendar project include', type: :feature, js: true do page.refresh work_package_view.expect_event task - work_package_view.expect_event sub_bug, present: false + work_package_view.expect_event sub_bug, present: true work_package_view.expect_event sub_sub_bug work_package_view.expect_event other_task work_package_view.expect_event other_other_task diff --git a/modules/team_planner/spec/features/team_planner_project_include_spec.rb b/modules/team_planner/spec/features/team_planner_project_include_spec.rb index 01ac4467cb..107c704de5 100644 --- a/modules/team_planner/spec/features/team_planner_project_include_spec.rb +++ b/modules/team_planner/spec/features/team_planner_project_include_spec.rb @@ -76,8 +76,8 @@ describe 'Team planner project include', type: :feature, js: true do work_package_view.within_lane(user) do work_package_view.expect_event task - work_package_view.expect_event sub_bug, present: false - work_package_view.expect_event sub_sub_bug, present: false + work_package_view.expect_event sub_bug, present: true + work_package_view.expect_event sub_sub_bug, present: true end work_package_view.within_lane(other_user) do @@ -92,7 +92,7 @@ describe 'Team planner project include', type: :feature, js: true do work_package_view.within_lane(user) do work_package_view.expect_event task - work_package_view.expect_event sub_bug, present: false + work_package_view.expect_event sub_bug, present: true work_package_view.expect_event sub_sub_bug end @@ -115,7 +115,7 @@ describe 'Team planner project include', type: :feature, js: true do work_package_view.within_lane(user) do work_package_view.expect_event task - work_package_view.expect_event sub_bug, present: false + work_package_view.expect_event sub_bug, present: true work_package_view.expect_event sub_sub_bug end end diff --git a/spec/features/work_packages/table/work_packages_table_project_include_spec.rb b/spec/features/work_packages/table/work_packages_table_project_include_spec.rb index 867a8c6159..e77c4a4fee 100644 --- a/spec/features/work_packages/table/work_packages_table_project_include_spec.rb +++ b/spec/features/work_packages/table/work_packages_table_project_include_spec.rb @@ -45,29 +45,27 @@ describe 'Calendar project include', type: :feature, js: true do dropdown.click_button 'Apply' dropdown.expect_closed - work_package_view.expect_work_package_listed(task, other_task) - work_package_view.ensure_work_package_not_listed!(sub_bug, sub_sub_bug, other_other_task) + work_package_view.expect_work_package_listed(task, other_task, sub_bug, sub_sub_bug) + work_package_view.ensure_work_package_not_listed!(other_other_task) dropdown.toggle! dropdown.toggle_checkbox(sub_sub_project.id) dropdown.click_button 'Apply' dropdown.expect_count 2 - work_package_view.expect_work_package_listed(task, other_task, sub_sub_bug) - work_package_view.ensure_work_package_not_listed!(sub_bug, other_other_task) + work_package_view.expect_work_package_listed(task, other_task, sub_sub_bug, sub_bug) + work_package_view.ensure_work_package_not_listed!(other_other_task) dropdown.toggle! dropdown.toggle_checkbox(other_project.id) dropdown.click_button 'Apply' dropdown.expect_count 3 - work_package_view.expect_work_package_listed(task, other_task, sub_sub_bug, other_other_task) - work_package_view.ensure_work_package_not_listed!(sub_bug) + work_package_view.expect_work_package_listed(task, other_task, sub_sub_bug, sub_bug, other_other_task) page.refresh - work_package_view.expect_work_package_listed(task, other_task, sub_sub_bug, other_other_task) - work_package_view.ensure_work_package_not_listed!(sub_bug) + work_package_view.expect_work_package_listed(task, other_task, sub_bug, sub_sub_bug, other_other_task) end end end diff --git a/spec/features/work_packages/timeline/timeline_labels_spec.rb b/spec/features/work_packages/timeline/timeline_labels_spec.rb index dd06bc21d5..8dc02e4487 100644 --- a/spec/features/work_packages/timeline/timeline_labels_spec.rb +++ b/spec/features/work_packages/timeline/timeline_labels_spec.rb @@ -135,9 +135,9 @@ RSpec.feature 'Work package timeline labels', # Check the query query = Query.last - expect(query.timeline_labels).to eq 'left' => 'assignee', - 'right' => 'type', - 'farRight' => 'status' + expect(query.timeline_labels).to eq left: 'assignee', + right: 'type', + farRight: 'status' # Revisit page wp_timeline.visit_query query diff --git a/spec/lib/api/v3/queries/query_representer_parsing_spec.rb b/spec/lib/api/v3/queries/query_representer_parsing_spec.rb index 724c0c74cf..160cc520fa 100644 --- a/spec/lib/api/v3/queries/query_representer_parsing_spec.rb +++ b/spec/lib/api/v3/queries/query_representer_parsing_spec.rb @@ -31,7 +31,7 @@ require 'spec_helper' describe ::API::V3::Queries::QueryRepresenter, 'parsing' do include ::API::V3::Utilities::PathHelper - let(:query) { build_stubbed(:query, project: project) } + let(:query) { ::API::ParserStruct.new } let(:project) { build_stubbed(:project) } let(:user) { build_stubbed(:user) } let(:representer) do @@ -82,10 +82,7 @@ describe ::API::V3::Queries::QueryRepresenter, 'parsing' do end it 'unsets group_by' do - expect(query).to be_grouped expect(query.group_by).to eq('project') - - expect(subject).not_to be_grouped end end @@ -115,20 +112,20 @@ describe ::API::V3::Queries::QueryRepresenter, 'parsing' do end before do - allow(query).to receive(:new_record?).and_return(new_record) + allow(query).to receive(:persisted?).and_return(persisted) end context 'if query is new' do - let(:new_record) { true } + let(:persisted) { nil } it 'sets ordered_work_packages' do - order = subject.ordered_work_packages.map { |el| [el.work_package_id, el.position] } - expect(order).to match_array [[50, 0], [38, 1234], [102, 81234123]] + order = subject.ordered_work_packages + expect(order).to eq({ '50' => 0, '38' => 1234, '102' => 81234123 }) end end context 'if query is not new' do - let(:new_record) { false } + let(:persisted) { true } it 'sets ordered_work_packages' do allow(query) 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 d1f77b4995..d6c57c737e 100644 --- a/spec/models/queries/work_packages/filter/project_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/project_filter_spec.rb @@ -106,13 +106,29 @@ describe Queries::WorkPackages::Filter::ProjectFilter, type: :model do end describe '#value_objects' do + let(:selected) { visible_projects.first } + let(:visible_descendants) { [] } + let(:descendants) { double('Project', visible: visible_descendants) } # rubocop:disable RSpec/VerifiedDoubles + before do - instance.values = [visible_projects.first.id.to_s] + allow(selected).to receive(:descendants).and_return(descendants) + + instance.values = [selected.id.to_s] end it 'returns an array of projects' do expect(instance.value_objects) - .to match_array([visible_projects.first]) + .to match_array([selected]) + end + + context 'with a visible child' do + let(:child) { build_stubbed(:project, parent: selected, id: 2134) } + let(:visible_descendants) { [child] } + + it 'still only returns the parent object' do + expect(instance.value_objects) + .to match_array([selected]) + end end end end diff --git a/spec/models/query/results_project_filter_integration_spec.rb b/spec/models/query/results_project_filter_integration_spec.rb new file mode 100644 index 0000000000..7ff0f3e918 --- /dev/null +++ b/spec/models/query/results_project_filter_integration_spec.rb @@ -0,0 +1,150 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2022 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' + +describe ::Query::Results, 'Project filter integration', type: :model, with_mail: false do + let(:query) do + build(:query, + user: user, + project: parent_project).tap do |q| + q.filters.clear + end + end + let(:query_results) do + described_class.new query + end + + shared_let(:parent_project) { create :project } + shared_let(:child_project) { create :project, parent: parent_project } + + shared_let(:second_parent_project) { create :project } + shared_let(:second_child_project) { create :project, parent: second_parent_project } + + shared_let(:user) do + create(:user, + firstname: 'user', + lastname: '1', + member_in_projects: [parent_project, child_project, second_parent_project, second_child_project], + member_with_permissions: [:view_work_packages]) + end + + shared_let(:parent_wp) { create :work_package, project: parent_project } + shared_let(:child_wp) { create :work_package, project: child_project } + + shared_let(:second_parent_wp) { create :work_package, project: second_parent_project } + shared_let(:second_child_wp) { create :work_package, project: second_child_project } + + before do + login_as user + end + + describe 'both parent projects selected' do + before do + query.add_filter 'project_id', '=', [parent_project.id, second_parent_project.id] + end + + context 'when subprojects included', with_settings: { display_subprojects_work_packages: true } do + it 'shows the sub work packages' do + expect(query_results.work_packages).to match_array [parent_wp, child_wp, second_parent_wp, second_child_wp] + end + end + + context 'when subprojects not included', with_settings: { display_subprojects_work_packages: false } do + it 'does not show the sub work packages' do + expect(query_results.work_packages).to match_array [parent_wp, second_parent_wp] + end + end + + context 'when subprojects explicitly disabled' do + before do + query.include_subprojects = false + end + + it 'does not show the sub work packages' do + expect(query_results.work_packages).to match_array [parent_wp, second_parent_wp] + end + end + end + + describe 'one parent projects selected' do + before do + query.add_filter 'project_id', '=', [second_parent_project.id] + end + + context 'when subprojects included', with_settings: { display_subprojects_work_packages: true } do + it 'shows the sub work packages' do + expect(query_results.work_packages).to match_array [second_parent_wp, second_child_wp] + end + end + + context 'when subprojects not included', with_settings: { display_subprojects_work_packages: false } do + it 'does not show the sub work packages' do + expect(query_results.work_packages).to match_array [second_parent_wp] + end + end + + context 'when subprojects explicitly disabled' do + before do + query.include_subprojects = false + end + + it 'does not show the sub work packages' do + expect(query_results.work_packages).to match_array [second_parent_wp] + end + end + end + + describe 'one parent and one other child selected' do + before do + query.add_filter 'project_id', '=', [child_project.id, second_parent_project.id] + end + + context 'when subprojects included', with_settings: { display_subprojects_work_packages: true } do + it 'shows the sub work packages' do + expect(query_results.work_packages).to match_array [child_wp, second_parent_wp, second_child_wp] + end + end + + context 'when subprojects not included', with_settings: { display_subprojects_work_packages: false } do + it 'does not show the sub work packages' do + expect(query_results.work_packages).to match_array [child_wp, second_parent_wp] + end + end + + context 'when subprojects explicitly disabled' do + before do + query.include_subprojects = false + end + + it 'does not show the sub work packages' do + expect(query_results.work_packages).to match_array [child_wp, second_parent_wp] + end + end + end +end diff --git a/spec/services/principals/replace_references_service_call_integration_spec.rb b/spec/services/principals/replace_references_service_call_integration_spec.rb index 45817a214d..59f666cf0d 100644 --- a/spec/services/principals/replace_references_service_call_integration_spec.rb +++ b/spec/services/principals/replace_references_service_call_integration_spec.rb @@ -380,7 +380,13 @@ describe Principals::ReplaceReferencesService, '#call', type: :model do context 'with Query' do it_behaves_like 'rewritten record', :query, - :user_id + :user_id do + let(:attributes) do + { + include_subprojects: true + } + end + end end context 'with CostQuery' do From bd67f1e9bcea7927d57647186a9aaa059ca58795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 28 Mar 2022 14:58:13 +0200 Subject: [PATCH 42/60] Add a ParserStruct overriding Enumerable#group_by --- app/services/api/parser_struct.rb | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 app/services/api/parser_struct.rb diff --git a/app/services/api/parser_struct.rb b/app/services/api/parser_struct.rb new file mode 100644 index 0000000000..407c78b727 --- /dev/null +++ b/app/services/api/parser_struct.rb @@ -0,0 +1,42 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2022 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module API + class ParserStruct < ::Hashie::Mash + ## + # TODO: Hashie::Mash extends from Hash and + # does not allow overriding any enumerable methods. + # + # This clashed with moving the queries services to BaseContracted, + # as we now use a +group_by+ attribute clashing with +Enumerable#group_by#. + # This redefines the method to ensure it works with queries, but does not solve the underlying issue. + def group_by + self[:group_by] + end + end +end From bcd8da890234eb3130e76377db298c0adf8e8fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 28 Mar 2022 11:08:36 +0200 Subject: [PATCH 43/60] Fix ordered_work_packages now that we're base service compatible --- app/services/queries/create_service.rb | 15 +-------------- .../queries/set_attributes_service.rb | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/app/services/queries/create_service.rb b/app/services/queries/create_service.rb index 3f0ffd5215..0d75b853ff 100644 --- a/app/services/queries/create_service.rb +++ b/app/services/queries/create_service.rb @@ -26,17 +26,4 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Queries::CreateService < ::BaseServices::Create - def after_validate(_params, call) - query = call.result - # Check which of the work package IDs exist - ids = query.ordered_work_packages.map(&:work_package_id) - existent_wps = WorkPackage.where(id: ids).pluck(:id).to_set - - query.ordered_work_packages = query.ordered_work_packages.select do |order_item| - existent_wps.include?(order_item.work_package_id) - end - - call - end -end +class Queries::CreateService < ::BaseServices::Create; end diff --git a/app/services/queries/set_attributes_service.rb b/app/services/queries/set_attributes_service.rb index 6488d5aed9..934e12bf53 100644 --- a/app/services/queries/set_attributes_service.rb +++ b/app/services/queries/set_attributes_service.rb @@ -27,6 +27,11 @@ #++ class Queries::SetAttributesService < ::BaseServices::SetAttributes + def set_attributes(params) + set_ordered_work_packages params.delete(:ordered_work_packages) + super + end + def set_default_attributes(_params) if model.include_subprojects.nil? model.include_subprojects = Setting.display_subprojects_work_packages? @@ -40,4 +45,18 @@ class Queries::SetAttributesService < ::BaseServices::SetAttributes model.user = user end end + + def set_ordered_work_packages(ordered_hash) + return if ordered_hash.nil? + + available = WorkPackage.where(id: ordered_hash.keys.map(&:to_s)).pluck(:id).to_set + + ordered_hash.each do |key, position| + # input keys are symbols due to hashie::mash, and AR doesn't like that + wp_id = key.to_s.to_i + next unless available.include?(wp_id.to_s.to_i) + + model.ordered_work_packages.build(work_package_id: wp_id, position: position) + end + end end From 8aefbc4a5d205dc62bac83f4ea3e914175608a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 4 Apr 2022 09:00:36 +0200 Subject: [PATCH 44/60] Actually use the ParserStruct --- app/services/api/parse_resource_params_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/api/parse_resource_params_service.rb b/app/services/api/parse_resource_params_service.rb index d2b06598eb..685e29e420 100644 --- a/app/services/api/parse_resource_params_service.rb +++ b/app/services/api/parse_resource_params_service.rb @@ -76,7 +76,7 @@ module API end def struct - Hashie::Mash.new + ParserStruct.new end def deep_to_h(value) From cb8fac1f14ede18c94735511cea7702c0af1ca2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 4 Apr 2022 09:05:46 +0200 Subject: [PATCH 45/60] Add validation and test for include_subprojects --- app/models/query.rb | 3 +++ spec/models/query_spec.rb | 22 +++++++++++++++++++ .../exporter/csv_integration_spec.rb | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/models/query.rb b/app/models/query.rb index 82e91ccb6d..d298dc512a 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -45,6 +45,9 @@ class Query < ApplicationRecord presence: true, length: { maximum: 255 } + validates :include_subprojects, + inclusion: [true, false] + validate :validate_work_package_filters validate :validate_columns validate :validate_sort_criteria diff --git a/spec/models/query_spec.rb b/spec/models/query_spec.rb index 8690bb1a92..15604a5a2e 100644 --- a/spec/models/query_spec.rb +++ b/spec/models/query_spec.rb @@ -60,6 +60,28 @@ describe Query, type: :model do expect(query.sort_criteria) .to match_array([['id', 'asc']]) end + + context 'with global subprojects include', with_settings: { display_subprojects_work_packages: true } do + it 'sets the include subprojects' do + expect(query.include_subprojects).to eq true + end + end + + context 'with global subprojects include', with_settings: { display_subprojects_work_packages: false } do + it 'sets the include subprojects' do + expect(query.include_subprojects).to eq false + end + end + end + + describe 'include_subprojects' do + let(:query) { Query.new name: 'foo' } + + it 'is required' do + expect(query).not_to be_valid + + expect(query.errors[:include_subprojects]).to include 'is not set to one of the allowed values.' + end end describe 'hidden' do diff --git a/spec/models/work_package/exporter/csv_integration_spec.rb b/spec/models/work_package/exporter/csv_integration_spec.rb index 4055bc28eb..b121be559c 100644 --- a/spec/models/work_package/exporter/csv_integration_spec.rb +++ b/spec/models/work_package/exporter/csv_integration_spec.rb @@ -41,7 +41,7 @@ describe WorkPackage::Exports::CSV, 'integration', type: :model do member_with_permissions: %i(view_work_packages)) end let(:query) do - Query.new(name: '_').tap do |query| + Query.new_default(name: '_').tap do |query| query.column_names = %i(subject assigned_to updated_at estimated_hours) end end From dc978f9bedfbf40a3ab073df0e00bbdd35b0a02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 4 Apr 2022 09:18:47 +0200 Subject: [PATCH 46/60] Use scope.where in project filter --- .../queries/work_packages/filter/project_filter.rb | 9 ++------- .../queries/work_packages/filter/project_filter_spec.rb | 6 ++++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/models/queries/work_packages/filter/project_filter.rb b/app/models/queries/work_packages/filter/project_filter.rb index dd7e88d9ee..3a4030a9c9 100644 --- a/app/models/queries/work_packages/filter/project_filter.rb +++ b/app/models/queries/work_packages/filter/project_filter.rb @@ -56,12 +56,7 @@ class Queries::WorkPackages::Filter::ProjectFilter < Queries::WorkPackages::Filt end def value_objects - available_projects = visible_projects.index_by(&:id) - - values - .flat_map { |project_id| available_projects[project_id.to_i] } - .compact - .uniq + visible_projects.where(id: values.map(&:to_i)) end def where @@ -71,7 +66,7 @@ class Queries::WorkPackages::Filter::ProjectFilter < Queries::WorkPackages::Filt private def visible_projects - @visible_projects ||= Project.visible.active + Project.visible.active 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 d6c57c737e..b847bea81c 100644 --- a/spec/models/queries/work_packages/filter/project_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/project_filter_spec.rb @@ -48,6 +48,12 @@ describe Queries::WorkPackages::Filter::ProjectFilter, type: :model do allow(visible_projects) .to receive(:exists?) .and_return(visible_projects.any?) + + allow(visible_projects) + .to receive(:where) do |args| + ids = args[:id] + visible_projects.select { |p| ids.include?(p.id) } + end end describe '#available?' do From b86a91154c81ad1f62f6fe0c97560adea420e1b0 Mon Sep 17 00:00:00 2001 From: Henriette Darge Date: Mon, 4 Apr 2022 10:21:14 +0200 Subject: [PATCH 47/60] [40257] What's new teaser block updated with team planner (#10370) * Add texts for "whats new teaser" for standard edition * Extend "What's new?" teaser with BIM changes. Co-authored-by: Wieland Lindenthal --- config/locales/js-en.yml | 28 ++++++++++-------- .../blocks/new-features.component.ts | 4 +-- frontend/src/assets/images/12_0_features.png | Bin 7426 -> 0 bytes frontend/src/assets/images/12_1_features.png | Bin 0 -> 10719 bytes 4 files changed, 18 insertions(+), 14 deletions(-) delete mode 100644 frontend/src/assets/images/12_0_features.png create mode 100644 frontend/src/assets/images/12_1_features.png diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 67257a8959..3a8dfe5928 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -317,27 +317,31 @@ en: learn_about: "Learn more about the new features" # Include the version to invalidate outdated translations in other locales. # Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_0': + '12_1': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-0-release + learn_about_link: https://www.openproject.org/openproject-12-1-release new_features_html: > The release contains various new features and improvements:
            -
          • With the new in-app notifications you receive all important updates directly in the application and don't get a flood of emails anymore.
          • -
          • The new notification center shows all the changes, including intuitive filter options, e.g. by reason for notification or projects. It even allows editing directly in a split view.
          • -
          • Improved notification settings allow to fine-tune for which actions and in which projects you want to receive a notification.
          • -
          • Email reminders can be configured to receive important updates via a daily email summary.
          • -
          • The work package auto-completer for relations now also shows additional information (project name, status, ...) to easily identify the respective work package.
          • +
          • A new team planner module (Enterprise feature) allows you to visually assign tasks to team members to get an overview of who is working on what.
          • +
          • We are releasing the basic agile boards for the Community version.
          • +
          • The "Include Projects" filter option makes it easier to add different projects to your views, such as for work packages, calendars, and team planners.
          • +
          • We added a new "Files" tab in the work package details to have all possible information attached to a work package together.
          • +
          • We created global roles for groups to assign these roles to groups and create superuser groups.
          • +
          • Project status was given more options to choose from.
          bim: - learn_about_link: https://www.openproject.org/blog/openproject-12-0-release + learn_about_link: https://www.openproject.org/openproject-12-1-release new_features_html: > The release contains various new features and improvements:
            -
          • Notification center: Reduce the number of mails you receive. Instead find and act upon all relevant notifications in one central place.
          • -
          • All uploaded IFC files can now also be downloaded.
          • -
          • The uploading and processing of IFC got faster and received better error messages.
          • -
          • The OpenProject Revit Add-in received many bug fixes.
          • +
          • In the BCF module you can now save your selected work package filters, columns etc. as views and share those with your team members.
          • +
          • A new team planner module (Enterprise feature) allows you to visually assign tasks to team members to get an overview of who is working on what.
          • +
          • We are releasing the basic agile boards for the Community version.
          • +
          • The "Include Projects" filter option makes it easier to add different projects to your views, such as for work packages, calendars, and team planners.
          • +
          • We added a new "Files" tab in the work package details to have all possible information attached to a work package together.
          • +
          • We created global roles for groups to assign these roles to groups and create superuser groups.
          • +
          • Project status was given more options to choose from.
          label_activate: "Activate" diff --git a/frontend/src/app/features/homescreen/blocks/new-features.component.ts b/frontend/src/app/features/homescreen/blocks/new-features.component.ts index 056340d7a9..35da0c378a 100644 --- a/frontend/src/app/features/homescreen/blocks/new-features.component.ts +++ b/frontend/src/app/features/homescreen/blocks/new-features.component.ts @@ -34,7 +34,7 @@ import { imagePath } from 'core-app/shared/helpers/images/path-helper'; export const homescreenNewFeaturesBlockSelector = 'homescreen-new-features-block'; // The key used in the I18n files to distinguish between versions. -const OpVersionI18n = '12_0'; +const OpVersionI18n = '12_1'; @Component({ template: ` @@ -64,7 +64,7 @@ const OpVersionI18n = '12_0'; export class HomescreenNewFeaturesBlockComponent { public isStandardEdition:boolean; - new_features_image = imagePath('12_0_features.png'); + new_features_image = imagePath('12_1_features.png'); public text = { newFeatures: this.i18n.t('js.label_new_features'), diff --git a/frontend/src/assets/images/12_0_features.png b/frontend/src/assets/images/12_0_features.png deleted file mode 100644 index 1d0764b2515b2b5ef89ca29f29234a8f9f1bc3dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7426 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 diff --git a/frontend/src/assets/images/12_1_features.png b/frontend/src/assets/images/12_1_features.png new file mode 100644 index 0000000000000000000000000000000000000000..ab2c554e173585234b61753c822428477153fe0f GIT binary patch literal 10719 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 NYv|v`-LilB-vH-*xPAZt literal 0 HcmV?d00001 From 01fbee73f7c6c56ca87fa9b2717f1aac4cdbd850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 4 Apr 2022 10:23:20 +0200 Subject: [PATCH 48/60] [41235] Fix indicator label --- config/locales/js-en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 3a8dfe5928..4686f9b6af 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -30,8 +30,8 @@ en: js: ajax: hide: "Hide" - loading: "Loading ..." - updating: "Updating ..." + loading: "Loading…" + updating: "Updating…" attachments: draggable_hint: | From ee82300e032fe2f74150ea55993da726b4f56b09 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 1 Apr 2022 15:50:52 +0200 Subject: [PATCH 49/60] bump aws-partitions --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d915b8b6e3..f03e531254 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -283,7 +283,7 @@ GEM awesome_nested_set (3.5.0) activerecord (>= 4.0.0, < 7.1) aws-eventstream (1.2.0) - aws-partitions (1.568.0) + aws-partitions (1.572.0) aws-sdk-core (3.130.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) From 9ca8cfd884bea33b32cba625439b57421d74f20c Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 1 Apr 2022 15:53:59 +0200 Subject: [PATCH 50/60] bump i18n-js --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index f03e531254..fa60629ae7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -548,7 +548,7 @@ GEM httpclient (2.8.3) i18n (1.10.0) concurrent-ruby (~> 1.0) - i18n-js (3.9.1) + i18n-js (3.9.2) i18n (>= 0.6.6) icalendar (2.7.1) ice_cube (~> 0.16) From ffa0ecd52fa2caee44c7ed512616c8679998e2f8 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 1 Apr 2022 15:54:10 +0200 Subject: [PATCH 51/60] bump kramdown --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index fa60629ae7..8487df87f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -569,7 +569,7 @@ GEM json_spec (1.1.5) multi_json (~> 1.0) rspec (>= 2.0, < 4.0) - kramdown (2.3.1) + kramdown (2.3.2) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) From f71c370c5382ac73d95134293dc1acc184f88ca9 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 1 Apr 2022 15:55:25 +0200 Subject: [PATCH 52/60] bump rspec-mocks --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8487df87f0..685134dc51 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -812,7 +812,7 @@ GEM rspec-expectations (3.11.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.11.0) - rspec-mocks (3.11.0) + rspec-mocks (3.11.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.11.0) rspec-rails (5.1.1) From 31b54f65280b55f39f0b995d8bc7da5d2b7d2760 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 1 Apr 2022 15:57:38 +0200 Subject: [PATCH 53/60] bump activerecord-import --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 117ec13637..403335abc8 100644 --- a/Gemfile +++ b/Gemfile @@ -32,7 +32,7 @@ ruby '~> 3.0.3' gem 'actionpack-xml_parser', '~> 2.0.0' gem 'activemodel-serializers-xml', '~> 1.0.1' -gem 'activerecord-import', '~> 1.3.0' +gem 'activerecord-import', '~> 1.4.0' gem 'activerecord-session_store', '~> 2.0.0' gem 'rails', '~> 6.1.4', '>= 6.1.4.6' gem 'responders', '~> 3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 685134dc51..6b4dee22b2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -241,7 +241,7 @@ GEM activerecord (6.1.5) activemodel (= 6.1.5) activesupport (= 6.1.5) - activerecord-import (1.3.0) + activerecord-import (1.4.0) activerecord (>= 4.2) activerecord-nulldb-adapter (0.8.0) activerecord (>= 5.2.0, < 7.1) @@ -976,7 +976,7 @@ PLATFORMS DEPENDENCIES actionpack-xml_parser (~> 2.0.0) activemodel-serializers-xml (~> 1.0.1) - activerecord-import (~> 1.3.0) + activerecord-import (~> 1.4.0) activerecord-nulldb-adapter (~> 0.8.0) activerecord-session_store (~> 2.0.0) acts_as_list (~> 1.0.1) From c57129e24682e492cb91cef75b7ae9be8b065220 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 1 Apr 2022 15:59:00 +0200 Subject: [PATCH 54/60] bump lograge --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 403335abc8..9e4cf76d04 100644 --- a/Gemfile +++ b/Gemfile @@ -137,7 +137,7 @@ gem 'okcomputer', '~> 1.18.1' gem 'gon', '~> 6.4.0' # Lograge to provide sane and non-verbose logging -gem 'lograge', '~> 0.11.0' +gem 'lograge', '~> 0.12.0' # Structured warnings to selectively disable them in production gem 'structured_warnings', '~> 0.4.0' diff --git a/Gemfile.lock b/Gemfile.lock index 6b4dee22b2..bf754d0ae1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -593,7 +593,7 @@ GEM omniauth (~> 1.1) omniauth-openid-connect (>= 0.2.1) rails (>= 3.2.21) - lograge (0.11.2) + lograge (0.12.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) @@ -1038,7 +1038,7 @@ DEPENDENCIES letter_opener listen (~> 3.7.0) livingstyleguide (~> 2.1.0) - lograge (~> 0.11.0) + lograge (~> 0.12.0) meta-tags (~> 2.16.0) mini_magick (~> 4.11.0) multi_json (~> 1.15.0) From bb44f6208a04db7afce99914e7ccbb862d6db1a1 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 4 Apr 2022 10:43:45 +0200 Subject: [PATCH 55/60] bump aws-partitions & loofah --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index bf754d0ae1..64b3fabe34 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -283,7 +283,7 @@ GEM awesome_nested_set (3.5.0) activerecord (>= 4.0.0, < 7.1) aws-eventstream (1.2.0) - aws-partitions (1.572.0) + aws-partitions (1.573.0) aws-sdk-core (3.130.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) @@ -598,7 +598,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.15.0) + loofah (2.16.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) From 881231d5d6dede4496849b71893e933e37083d8c Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 4 Apr 2022 10:45:50 +0200 Subject: [PATCH 56/60] bump excon & fugit --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 64b3fabe34..b7b6259750 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -453,7 +453,7 @@ GEM tzinfo eventmachine (1.2.7) eventmachine_httpserver (0.2.1) - excon (0.92.0) + excon (0.92.2) factory_bot (6.2.1) activesupport (>= 5.0.0) factory_bot_rails (6.2.0) @@ -507,8 +507,8 @@ GEM formatador (1.1.0) friendly_id (5.4.2) activerecord (>= 4.0.0) - fugit (1.5.2) - et-orbi (~> 1.1, >= 1.1.8) + fugit (1.5.3) + et-orbi (~> 1, >= 1.2.7) raabro (~> 1.4) fuubar (2.5.1) rspec-core (~> 3.0) From 98e6f40b13b035b4626fdd5f4964d8d05e14b587 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 4 Apr 2022 10:46:42 +0200 Subject: [PATCH 57/60] bump json_schemer --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index b7b6259750..f6e717a922 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -561,7 +561,7 @@ GEM activesupport (>= 4.2) aes_key_wrap bindata - json_schemer (0.2.19) + json_schemer (0.2.20) ecma-re-validator (~> 0.3) hana (~> 1.3) regexp_parser (~> 2.0) From 58b64686d835e7c073cf4b1396afbf1377d0853b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 4 Apr 2022 09:30:21 +0200 Subject: [PATCH 58/60] Add create contract spec for queries --- .../contracts/queries/create_contract_spec.rb | 62 +++++++++++++++++++ .../queries/shared_contract_examples.rb | 52 ++++++++++++++++ .../contracts/queries/update_contract_spec.rb | 32 +++------- spec/models/query_spec.rb | 6 +- 4 files changed, 124 insertions(+), 28 deletions(-) create mode 100644 spec/contracts/queries/create_contract_spec.rb create mode 100644 spec/contracts/queries/shared_contract_examples.rb diff --git a/spec/contracts/queries/create_contract_spec.rb b/spec/contracts/queries/create_contract_spec.rb new file mode 100644 index 0000000000..df49504a04 --- /dev/null +++ b/spec/contracts/queries/create_contract_spec.rb @@ -0,0 +1,62 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2022 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' +require 'contracts/shared/model_contract_shared_context' +require_relative 'shared_contract_examples' + +describe Queries::CreateContract do + include_context 'ModelContract shared context' + include_context 'with queries contract' + + describe 'include subprojects' do + let(:query) do + Query.new name: 'foo', + include_subprojects: include_subprojects, + project: project + end + + context 'when true' do + let(:include_subprojects) { true } + + it_behaves_like 'contract is valid' + end + + context 'when falsea' do + let(:include_subprojects) { false } + + it_behaves_like 'contract is valid' + end + + context 'when nil' do + let(:include_subprojects) { nil } + + it_behaves_like 'contract is invalid', include_subprojects: %i[inclusion] + end + end +end diff --git a/spec/contracts/queries/shared_contract_examples.rb b/spec/contracts/queries/shared_contract_examples.rb new file mode 100644 index 0000000000..90d0d20938 --- /dev/null +++ b/spec/contracts/queries/shared_contract_examples.rb @@ -0,0 +1,52 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2022 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' +require 'contracts/shared/model_contract_shared_context' + +shared_context 'with queries contract' do + let(:project) { build_stubbed :project } + let(:query) do + build_stubbed(:query, project: project, public: public, user: user) + end + + let(:current_user) do + build_stubbed(:user) do |user| + allow(user) + .to receive(:allowed_to?) do |permission, permission_project| + permissions.include?(permission) && project == permission_project + end + end + end + let(:contract) { described_class.new(query, current_user) } + + before do + # Assume project is always visible + allow(contract).to receive(:project_visible?).and_return true + end +end diff --git a/spec/contracts/queries/update_contract_spec.rb b/spec/contracts/queries/update_contract_spec.rb index 37c2a8a34e..30735721b6 100644 --- a/spec/contracts/queries/update_contract_spec.rb +++ b/spec/contracts/queries/update_contract_spec.rb @@ -28,29 +28,11 @@ require 'spec_helper' require 'contracts/shared/model_contract_shared_context' +require_relative 'shared_contract_examples' describe Queries::UpdateContract do include_context 'ModelContract shared context' - - let(:project) { build_stubbed :project } - let(:query) do - build_stubbed(:query, project: project, public: public, user: user) - end - - let(:current_user) do - build_stubbed(:user) do |user| - allow(user) - .to receive(:allowed_to?) do |permission, permission_project| - permissions.include?(permission) && project == permission_project - end - end - end - let(:contract) { described_class.new(query, current_user) } - - before do - # Assume project is always visible - allow(contract).to receive(:project_visible?).and_return true - end + include_context 'with queries contract' describe 'private query' do let(:public) { false } @@ -58,13 +40,13 @@ describe Queries::UpdateContract do context 'when user is author' do let(:user) { current_user } - context 'user has no permission to save' do + context 'when user has no permission to save' do let(:permissions) { %i(edit_work_packages) } it_behaves_like 'contract user is unauthorized' end - context 'user has permission to save' do + context 'when user has permission to save' do let(:permissions) { %i(save_queries) } it_behaves_like 'contract is valid' @@ -83,19 +65,19 @@ describe Queries::UpdateContract do let(:public) { true } let(:user) { nil } - context 'user has no permission to save' do + context 'when user has no permission to save' do let(:permissions) { %i(invalid_permission) } it_behaves_like 'contract user is unauthorized' end - context 'user has no permission to manage public' do + context 'when user has no permission to manage public' do let(:permissions) { %i(manage_public_queries) } it_behaves_like 'contract is valid' end - context 'user has permission to save only own' do + context 'when user has permission to save only own' do let(:permissions) { %i(save_queries) } it_behaves_like 'contract user is unauthorized' diff --git a/spec/models/query_spec.rb b/spec/models/query_spec.rb index 15604a5a2e..c90fb1583a 100644 --- a/spec/models/query_spec.rb +++ b/spec/models/query_spec.rb @@ -63,19 +63,19 @@ describe Query, type: :model do context 'with global subprojects include', with_settings: { display_subprojects_work_packages: true } do it 'sets the include subprojects' do - expect(query.include_subprojects).to eq true + expect(query.include_subprojects).to be true end end context 'with global subprojects include', with_settings: { display_subprojects_work_packages: false } do it 'sets the include subprojects' do - expect(query.include_subprojects).to eq false + expect(query.include_subprojects).to be false end end end describe 'include_subprojects' do - let(:query) { Query.new name: 'foo' } + let(:query) { described_class.new name: 'foo' } it 'is required' do expect(query).not_to be_valid From 4785387bf61c13d43c45244541c68c7fa70fd26e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 4 Apr 2022 13:56:02 +0200 Subject: [PATCH 59/60] [41119] Correctly block parents in team planner https://community.openproject.org/wp/41119 --- .../team-planner/calendar-drag-drop.service.ts | 7 +++---- .../team-planner/page/team-planner-page.component.ts | 2 ++ .../team-planner/planner/team-planner.component.ts | 1 - 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/features/team-planner/team-planner/calendar-drag-drop.service.ts b/frontend/src/app/features/team-planner/team-planner/calendar-drag-drop.service.ts index 98ede1dd67..623b002b37 100644 --- a/frontend/src/app/features/team-planner/team-planner/calendar-drag-drop.service.ts +++ b/frontend/src/app/features/team-planner/team-planner/calendar-drag-drop.service.ts @@ -10,6 +10,7 @@ import { BehaviorSubject } from 'rxjs'; import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; import { AuthorisationService } from 'core-app/core/model-auth/model-auth.service'; import { I18nService } from 'core-app/core/i18n/i18n.service'; +import { OpCalendarService } from 'core-app/features/calendar/op-calendar.service'; @Injectable() export class CalendarDragDropService { @@ -27,6 +28,7 @@ export class CalendarDragDropService { constructor( readonly authorisation:AuthorisationService, readonly schemaCache:SchemaCacheService, + readonly calendarService:OpCalendarService, readonly I18n:I18nService, ) { } @@ -89,10 +91,7 @@ export class CalendarDragDropService { return { disabled: true, reason: this.text.draggingDisabled.permissionDenied }; } - const schema = this.schemaCache.of(workPackage); - const schemaEditable = schema.isAttributeEditable('startDate') && schema.isAttributeEditable('dueDate'); - - if (!schemaEditable) { + if (!this.calendarService.dateEditable(workPackage)) { return { disabled: true, reason: this.text.draggingDisabled.fallback }; } diff --git a/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts b/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts index a861262a76..60ea610c42 100644 --- a/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts +++ b/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts @@ -24,6 +24,7 @@ import { import { teamPlannerEventAdded } from 'core-app/features/team-planner/team-planner/planner/team-planner.actions'; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator'; import { ActionsService } from 'core-app/core/state/actions/actions.service'; +import { OpCalendarService } from 'core-app/features/calendar/op-calendar.service'; @EffectHandler @Component({ @@ -34,6 +35,7 @@ import { ActionsService } from 'core-app/core/state/actions/actions.service'; changeDetection: ChangeDetectionStrategy.OnPush, providers: [ QueryParamListenerService, + OpCalendarService, CalendarDragDropService, ], }) diff --git a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts index fb22ec840e..e48ba7c2da 100644 --- a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts +++ b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts @@ -90,7 +90,6 @@ import { LoadingIndicatorService } from 'core-app/core/loading-indicator/loading changeDetection: ChangeDetectionStrategy.OnPush, providers: [ EventViewLookupService, - OpCalendarService, ], }) export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit, OnDestroy { From f818d16ce134ff6ec9318d63c3c06c67d5ed90c2 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 4 Apr 2022 15:02:04 +0200 Subject: [PATCH 60/60] adapt overwritten method name --- lib/open_project/text_formatting/filters/sanitization_filter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/open_project/text_formatting/filters/sanitization_filter.rb b/lib/open_project/text_formatting/filters/sanitization_filter.rb index cd007c653e..d1b016654c 100644 --- a/lib/open_project/text_formatting/filters/sanitization_filter.rb +++ b/lib/open_project/text_formatting/filters/sanitization_filter.rb @@ -29,7 +29,7 @@ module OpenProject::TextFormatting module Filters class SanitizationFilter < HTML::Pipeline::SanitizationFilter - def whitelist + def allowlist base = super Sanitize::Config.merge(