From 374e1c6cf38ee2f23197ead2bd19bea822e86b1c Mon Sep 17 00:00:00 2001 From: ulferts Date: Wed, 30 Mar 2022 17:14:35 +0200 Subject: [PATCH 01/80] move null_db_fallback to constants --- {lib => config/constants}/open_project/null_db_fallback.rb | 0 config/initializers/05-null_db_fallback.rb | 1 + 2 files changed, 1 insertion(+) rename {lib => config/constants}/open_project/null_db_fallback.rb (100%) diff --git a/lib/open_project/null_db_fallback.rb b/config/constants/open_project/null_db_fallback.rb similarity index 100% rename from lib/open_project/null_db_fallback.rb rename to config/constants/open_project/null_db_fallback.rb diff --git a/config/initializers/05-null_db_fallback.rb b/config/initializers/05-null_db_fallback.rb index 0478dd83cf..21ad5e5539 100644 --- a/config/initializers/05-null_db_fallback.rb +++ b/config/initializers/05-null_db_fallback.rb @@ -30,5 +30,6 @@ # As initializers and other parts of the boot sequence rely on calls accessing # the DB, the null db gem is used to fake the existence of a database in cases where # the db has not been created yet. +require Rails.root.join('config/constants/open_project/null_db_fallback') OpenProject::NullDbFallback.fallback From aedfa6ba2c421b49b74ef677f8745247b23da0c5 Mon Sep 17 00:00:00 2001 From: ulferts Date: Wed, 30 Mar 2022 22:00:09 +0200 Subject: [PATCH 02/80] avoid reference to Project in initializer --- app/models/projects/activity.rb | 10 +---- .../open_project/project_activity.rb | 41 ++++++++++++++++++ config/constants/project_activity.rb | 43 ------------------- config/initializers/activity.rb | 28 ++++++------ modules/budgets/lib/budgets/engine.rb | 4 +- modules/costs/lib/costs/engine.rb | 4 +- .../lib/open_project/meeting/engine.rb | 4 +- 7 files changed, 63 insertions(+), 71 deletions(-) create mode 100644 config/constants/open_project/project_activity.rb delete mode 100644 config/constants/project_activity.rb diff --git a/app/models/projects/activity.rb b/app/models/projects/activity.rb index 36961dee53..6f34bd5a30 100644 --- a/app/models/projects/activity.rb +++ b/app/models/projects/activity.rb @@ -26,23 +26,15 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require Rails.root.join('config/constants/project_activity') - module Projects::Activity def self.included(base) base.send :extend, ActivityScopes end module ActivityScopes - def register_latest_project_activity(on:, attribute:, chain: []) - Constants::ProjectActivity.register(on: on, - chain: chain, - attribute: attribute) - end - def latest_project_activity @latest_project_activity ||= - Constants::ProjectActivity.registered.map do |params| + OpenProject::ProjectActivity.registered.map do |params| build_latest_project_activity_for(on: params[:on].constantize, chain: Array(params[:chain]).map(&:constantize), attribute: params[:attribute]) diff --git a/config/constants/open_project/project_activity.rb b/config/constants/open_project/project_activity.rb new file mode 100644 index 0000000000..394a712fc0 --- /dev/null +++ b/config/constants/open_project/project_activity.rb @@ -0,0 +1,41 @@ +# OpenProject is an open source project management software. +# Copyright (C) 2010-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 OpenProject + module ProjectActivity + class << self + def register(on:, attribute:, chain: []) + @registered ||= Set.new + + @registered << { on: on, + chain: chain, + attribute: attribute } + end + + attr_reader :registered + end + end +end diff --git a/config/constants/project_activity.rb b/config/constants/project_activity.rb deleted file mode 100644 index faaff8c380..0000000000 --- a/config/constants/project_activity.rb +++ /dev/null @@ -1,43 +0,0 @@ -#-- 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 Constants - module ProjectActivity - class << self - def register(on:, attribute:, chain: []) - @registered ||= Set.new - - @registered << { on: on, - chain: chain, - attribute: attribute } - end - - attr_reader :registered - end - end -end diff --git a/config/initializers/activity.rb b/config/initializers/activity.rb index 555266553a..c687f92378 100644 --- a/config/initializers/activity.rb +++ b/config/initializers/activity.rb @@ -26,6 +26,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ +require_relative '../constants/open_project/project_activity' + Rails.application.reloader.to_prepare do OpenProject::Activity.map do |activity| activity.register :work_packages, class_name: '::Activities::WorkPackageActivityProvider' @@ -38,21 +40,21 @@ Rails.application.reloader.to_prepare do default: false end - Project.register_latest_project_activity on: 'WorkPackage', - attribute: :updated_at + OpenProject::ProjectActivity.register on: 'WorkPackage', + attribute: :updated_at - Project.register_latest_project_activity on: 'News', - attribute: :updated_at + OpenProject::ProjectActivity.register on: 'News', + attribute: :updated_at - Project.register_latest_project_activity on: 'Changeset', - chain: 'Repository', - attribute: :committed_on + OpenProject::ProjectActivity.register on: 'Changeset', + chain: 'Repository', + attribute: :committed_on - Project.register_latest_project_activity on: 'WikiContent', - chain: %w(Wiki WikiPage), - attribute: :updated_at + OpenProject::ProjectActivity.register on: 'WikiContent', + chain: %w(Wiki WikiPage), + attribute: :updated_at - Project.register_latest_project_activity on: 'Message', - chain: 'Forum', - attribute: :updated_at + OpenProject::ProjectActivity.register on: 'Message', + chain: 'Forum', + attribute: :updated_at end diff --git a/modules/budgets/lib/budgets/engine.rb b/modules/budgets/lib/budgets/engine.rb index 9e5dcd24d1..93a566c78a 100644 --- a/modules/budgets/lib/budgets/engine.rb +++ b/modules/budgets/lib/budgets/engine.rb @@ -48,8 +48,8 @@ module Budgets end config.to_prepare do - Project.register_latest_project_activity on: 'Budget', - attribute: :updated_at + OpenProject::ProjectActivity.register on: 'Budget', + attribute: :updated_at # Add to the budget to the costs group ::Type.add_default_mapping(:costs, :budget) diff --git a/modules/costs/lib/costs/engine.rb b/modules/costs/lib/costs/engine.rb index a10b13a8af..09496bde7c 100644 --- a/modules/costs/lib/costs/engine.rb +++ b/modules/costs/lib/costs/engine.rb @@ -249,8 +249,8 @@ module Costs end config.to_prepare do - Project.register_latest_project_activity on: 'TimeEntry', - attribute: :updated_at + OpenProject::ProjectActivity.register on: 'TimeEntry', + attribute: :updated_at Costs::Patches::MembersPatch.mixin! diff --git a/modules/meeting/lib/open_project/meeting/engine.rb b/modules/meeting/lib/open_project/meeting/engine.rb index 1c9387f861..4900c4f4a2 100644 --- a/modules/meeting/lib/open_project/meeting/engine.rb +++ b/modules/meeting/lib/open_project/meeting/engine.rb @@ -81,8 +81,8 @@ module OpenProject::Meeting end config.to_prepare do - Project.register_latest_project_activity on: 'Meeting', - attribute: :updated_at + OpenProject::ProjectActivity.register on: 'Meeting', + attribute: :updated_at PermittedParams.permit(:search, :meetings) end From 6ad27131418cf48721c9d5530fe661eb6052c46a Mon Sep 17 00:00:00 2001 From: ulferts Date: Thu, 31 Mar 2022 08:55:47 +0200 Subject: [PATCH 03/80] avoid autoloading ApplicationHelper during initialization --- config/initializers/00-core_plugins.rb | 4 ++++ {lib => constants}/redmine/diff.rb | 0 {lib => constants}/redmine/diff/array_string_diff.rb | 0 {lib => constants}/redmine/diff/diffable.rb | 0 {lib => constants}/redmine/i18n.rb | 0 lib/open_project.rb | 2 -- lib/open_project/plugins/acts_as_op_engine.rb | 5 +++-- .../auth_plugins/lib/open_project/auth_plugins/engine.rb | 4 ++-- modules/backlogs/lib/open_project/backlogs/engine.rb | 6 +++--- .../backlogs/{hooks.rb => hooks/layout_hook.rb} | 0 modules/bim/lib/open_project/bim/engine.rb | 7 +------ modules/budgets/lib/budgets/engine.rb | 5 ++--- modules/xls_export/lib/open_project/xls_export/engine.rb | 9 +++------ 13 files changed, 18 insertions(+), 24 deletions(-) rename {lib => constants}/redmine/diff.rb (100%) rename {lib => constants}/redmine/diff/array_string_diff.rb (100%) rename {lib => constants}/redmine/diff/diffable.rb (100%) rename {lib => constants}/redmine/i18n.rb (100%) rename modules/backlogs/lib/open_project/backlogs/{hooks.rb => hooks/layout_hook.rb} (100%) diff --git a/config/initializers/00-core_plugins.rb b/config/initializers/00-core_plugins.rb index 3e5a105988..b11d42c74d 100644 --- a/config/initializers/00-core_plugins.rb +++ b/config/initializers/00-core_plugins.rb @@ -26,6 +26,10 @@ # See COPYRIGHT and LICENSE files for more details. #++ +require Rails.root.join('constants/redmine/i18n') +require Rails.root.join('constants/redmine/diff') +require Rails.root.join('constants/redmine/diff/diffable') + # Loads the core plugins located in lib/plugins Dir.glob(File.join(Rails.root, 'lib/plugins/*')).sort.each do |directory| if File.directory?(directory) diff --git a/lib/redmine/diff.rb b/constants/redmine/diff.rb similarity index 100% rename from lib/redmine/diff.rb rename to constants/redmine/diff.rb diff --git a/lib/redmine/diff/array_string_diff.rb b/constants/redmine/diff/array_string_diff.rb similarity index 100% rename from lib/redmine/diff/array_string_diff.rb rename to constants/redmine/diff/array_string_diff.rb diff --git a/lib/redmine/diff/diffable.rb b/constants/redmine/diff/diffable.rb similarity index 100% rename from lib/redmine/diff/diffable.rb rename to constants/redmine/diff/diffable.rb diff --git a/lib/redmine/i18n.rb b/constants/redmine/i18n.rb similarity index 100% rename from lib/redmine/i18n.rb rename to constants/redmine/i18n.rb diff --git a/lib/open_project.rb b/lib/open_project.rb index 196c8b3376..7fb54ec40a 100644 --- a/lib/open_project.rb +++ b/lib/open_project.rb @@ -33,8 +33,6 @@ require 'open_project/logging' require 'open_project/patches' require 'open_project/mime_type' require 'open_project/custom_styles/design' -require 'open_project/hook' -require 'open_project/hooks' require 'redmine/plugin' require 'csv' diff --git a/lib/open_project/plugins/acts_as_op_engine.rb b/lib/open_project/plugins/acts_as_op_engine.rb index 14c65bd4d0..fffe207385 100644 --- a/lib/open_project/plugins/acts_as_op_engine.rb +++ b/lib/open_project/plugins/acts_as_op_engine.rb @@ -62,8 +62,9 @@ module OpenProject::Plugins app.config.i18n.load_path += Dir[config.root.join('config', 'locales', 'crowdin', '*.{rb,yml}').to_s] end - initializer "#{engine_name}.register_cell_view_paths" do |_app| - pathname = config.root.join("app/cells/views") + current_engine = self + config.to_prepare do + pathname = current_engine.root.join("app/cells/views") ::RailsCell.view_paths << pathname.to_path if pathname.exist? end diff --git a/modules/auth_plugins/lib/open_project/auth_plugins/engine.rb b/modules/auth_plugins/lib/open_project/auth_plugins/engine.rb index 748c4d66cb..f33ddcac09 100644 --- a/modules/auth_plugins/lib/open_project/auth_plugins/engine.rb +++ b/modules/auth_plugins/lib/open_project/auth_plugins/engine.rb @@ -38,8 +38,8 @@ module OpenProject::AuthPlugins author_url: 'https://www.openproject.org', bundled: true - initializer 'auth_plugins.register_hooks' do - require 'open_project/auth_plugins/hooks' + config.to_prepare do + OpenProject::AuthPlugins::Hooks end end end diff --git a/modules/backlogs/lib/open_project/backlogs/engine.rb b/modules/backlogs/lib/open_project/backlogs/engine.rb index d7a6c75e48..67b164c788 100644 --- a/modules/backlogs/lib/open_project/backlogs/engine.rb +++ b/modules/backlogs/lib/open_project/backlogs/engine.rb @@ -175,9 +175,9 @@ module OpenProject::Backlogs "#{root}/backlogs_types/#{id}" end - initializer 'backlogs.register_hooks' do - require 'open_project/backlogs/hooks' - require 'open_project/backlogs/hooks/user_settings_hook' + config.to_prepare do + OpenProject::Backlogs::Hooks::LayoutHook + OpenProject::Backlogs::Hooks::UserSettingsHook end config.to_prepare do diff --git a/modules/backlogs/lib/open_project/backlogs/hooks.rb b/modules/backlogs/lib/open_project/backlogs/hooks/layout_hook.rb similarity index 100% rename from modules/backlogs/lib/open_project/backlogs/hooks.rb rename to modules/backlogs/lib/open_project/backlogs/hooks/layout_hook.rb diff --git a/modules/bim/lib/open_project/bim/engine.rb b/modules/bim/lib/open_project/bim/engine.rb index b1a304c731..3692724f5a 100644 --- a/modules/bim/lib/open_project/bim/engine.rb +++ b/modules/bim/lib/open_project/bim/engine.rb @@ -187,12 +187,7 @@ module OpenProject::Bim end config.to_prepare do - require_relative 'hooks' - end - - initializer 'bim.bcf.register_hooks' do - # don't use require_dependency to not reload hooks in development mode - require 'open_project/xls_export/hooks/work_package_hook' + OpenProject::Bim::Hooks::Hook end initializer 'bim.bcf.register_mimetypes' do diff --git a/modules/budgets/lib/budgets/engine.rb b/modules/budgets/lib/budgets/engine.rb index 93a566c78a..bddd87fdb2 100644 --- a/modules/budgets/lib/budgets/engine.rb +++ b/modules/budgets/lib/budgets/engine.rb @@ -42,9 +42,8 @@ module Budgets mount ::API::V3::Budgets::BudgetsByProjectAPI end - initializer 'budgets.register_hooks' do - # TODO: avoid hooks as this is part of the core now - require 'budgets/hooks/work_package_hook' + config.to_prepare do + Budgets::Hooks::WorkPackageHook end config.to_prepare do diff --git a/modules/xls_export/lib/open_project/xls_export/engine.rb b/modules/xls_export/lib/open_project/xls_export/engine.rb index 105795123a..d63be150d3 100644 --- a/modules/xls_export/lib/open_project/xls_export/engine.rb +++ b/modules/xls_export/lib/open_project/xls_export/engine.rb @@ -10,12 +10,9 @@ module OpenProject::XlsExport patches %i[CostReportsController] - initializer 'xls_export.register_hooks' do - # don't use require_dependency to not reload hooks in development mode - - require 'open_project/xls_export/hooks/cost_report_hook' - - require 'open_project/xls_export/hooks/work_package_hook' + config.to_prepare do + OpenProject::XlsExport::Hooks::CostReportHook + OpenProject::XlsExport::Hooks::WorkPackageHook end initializer 'xls_export.register_mimetypes' do From c794a9249bcef1422d60991fb5e81821e64f82a9 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 1 Apr 2022 08:58:56 +0200 Subject: [PATCH 04/80] defer autoloading permissions --- config/initializers/permissions.rb | 622 +++++++++--------- lib/redmine/plugin.rb | 5 +- .../lib/open_project/backlogs/engine.rb | 21 +- modules/bim/lib/open_project/bim/engine.rb | 6 +- modules/dashboards/lib/dashboards/engine.rb | 16 +- .../open_project/github_integration/engine.rb | 14 +- modules/overviews/lib/overviews/engine.rb | 20 +- .../lib/open_project/reporting/engine.rb | 16 +- 8 files changed, 367 insertions(+), 353 deletions(-) diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb index db64b77f62..7b19975a02 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -26,330 +26,330 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require 'open_project/access_control' - -OpenProject::AccessControl.map do |map| - map.project_module nil, order: 100 do - map.permission :add_project, - { projects: %i[new] }, - require: :loggedin, - global: true, - contract_actions: { projects: %i[create] } - - map.permission :create_backup, - { backups: %i[index] }, - require: :loggedin, - global: true, - enabled: -> { OpenProject::Configuration.backup_enabled? } - - map.permission :manage_user, - { - users: %i[index show new create edit update resend_invitation], - 'users/memberships': %i[create update destroy], - admin: %i[index] - }, - require: :loggedin, - global: true, - contract_actions: { users: %i[create read update] } - - map.permission :manage_placeholder_user, - { - placeholder_users: %i[index show new create edit update deletion_info destroy], - 'placeholder_users/memberships': %i[create update destroy], - admin: %i[index] - }, - require: :loggedin, - global: true, - contract_actions: { placeholder_users: %i[create read update] } - - map.permission :view_project, - { projects: [:show], - activities: [:index] }, - public: true - - map.permission :search_project, - { search: :index }, - public: true - - map.permission :edit_project, - { - 'projects/settings/general': %i[show], - 'projects/settings/storage': %i[show], - 'projects/templated': %i[create destroy], - 'projects/identifier': %i[show update] - }, - require: :member, - contract_actions: { projects: %i[update] } - - map.permission :select_project_modules, - { - 'projects/settings/modules': %i[show update] - }, - require: :member - - map.permission :manage_members, - { members: %i[index new create update destroy autocomplete_for_member] }, - require: :member, - dependencies: :view_members, - contract_actions: { members: %i[create update destroy] } - - map.permission :view_members, - { members: [:index] }, - contract_actions: { members: %i[read] } - - map.permission :manage_versions, - { - 'projects/settings/versions': [:show], - versions: %i[new create edit update close_completed destroy] - }, - require: :member - - map.permission :manage_types, - { - 'projects/settings/types': %i[show update] - }, - require: :member - - map.permission :select_custom_fields, - { - 'projects/settings/custom_fields': %i[show update] - }, - require: :member - - map.permission :add_subprojects, - { projects: %i[new] }, - require: :member - - map.permission :copy_projects, - { - projects: %i[copy] - }, - require: :member, - contract_actions: { projects: %i[copy] } - end - - map.project_module :work_package_tracking, order: 90 do |wpt| - wpt.permission :view_work_packages, - { - versions: %i[index show status_by], - journals: %i[index diff], - work_packages: %i[show index], - work_packages_api: [:get], - 'work_packages/reports': %i[report report_details] - }, - contract_actions: { work_packages: %i[read] } - - wpt.permission :add_work_packages, - {} - - wpt.permission :edit_work_packages, - { - 'work_packages/bulk': %i[edit update] - }, - require: :member, - dependencies: :view_work_packages - - wpt.permission :move_work_packages, - { 'work_packages/moves': %i[new create] }, - require: :loggedin, - dependencies: :view_work_packages - - wpt.permission :add_work_package_notes, - { - # FIXME: Although the endpoint is removed, the code checking whether a user - # is eligible to add work packages through the API still seems to rely on this. - journals: [:new] - }, - dependencies: :view_work_packages - - wpt.permission :edit_work_package_notes, - {}, - require: :loggedin, - dependencies: :view_work_packages - - wpt.permission :edit_own_work_package_notes, - {}, - require: :loggedin, - dependencies: :view_work_packages - - # WorkPackage categories - wpt.permission :manage_categories, - { - 'projects/settings/categories': [:show], - categories: %i[new create edit update destroy] - }, - require: :member - - wpt.permission :export_work_packages, - { - work_packages: %i[index all] - }, - dependencies: :view_work_packages - - wpt.permission :delete_work_packages, - { - work_packages: :destroy, - 'work_packages/bulk': :destroy - }, - require: :member, - dependencies: :view_work_packages - - wpt.permission :manage_work_package_relations, - { - work_package_relations: %i[create destroy] - }, - dependencies: :view_work_packages - - wpt.permission :manage_subtasks, - {}, - dependencies: :view_work_packages - # Queries - wpt.permission :manage_public_queries, - {}, - require: :member - - wpt.permission :save_queries, - {}, - require: :loggedin, - dependencies: :view_work_packages - # Watchers - wpt.permission :view_work_package_watchers, - {}, - dependencies: :view_work_packages - - wpt.permission :add_work_package_watchers, - {}, - dependencies: :view_work_packages - - wpt.permission :delete_work_package_watchers, - {}, - dependencies: :view_work_packages - - wpt.permission :assign_versions, - {}, - dependencies: :view_work_packages - - # A user having the following permission can become assignee and/or responsible of a work package. - # This is a passive permission in the sense that a user having the permission isn't eligible to perform - # actions but rather to have actions taken together with him/her. - wpt.permission :work_package_assigned, - {}, - require: :member, - contract_actions: { work_packages: %i[assigned] }, - grant_to_admin: false - end - - map.project_module :news do |news| - news.permission :view_news, - { news: %i[index show] }, - public: true - - news.permission :manage_news, - { - news: %i[new create edit update destroy preview], - 'news/comments': [:destroy] - }, - require: :member - - news.permission :comment_news, - { 'news/comments': :create } - end - - map.project_module :wiki do |wiki| - wiki.permission :view_wiki_pages, - { wiki: %i[index show special date_index] } - - wiki.permission :list_attachments, - { wiki: :list_attachments }, - require: :member - - wiki.permission :manage_wiki, - { wikis: %i[edit destroy] }, - require: :member - - wiki.permission :manage_wiki_menu, - { wiki_menu_items: %i[edit update select_main_menu_item replace_main_menu_item] }, - require: :member - - wiki.permission :rename_wiki_pages, - { wiki: :rename }, - require: :member - - wiki.permission :change_wiki_parent_page, - { wiki: %i[edit_parent_page update_parent_page] }, - require: :member - - wiki.permission :delete_wiki_pages, - { wiki: :destroy }, - require: :member - - wiki.permission :export_wiki_pages, - { wiki: [:export] } - - wiki.permission :view_wiki_edits, - { wiki: %i[history diff annotate] } - - wiki.permission :edit_wiki_pages, - { wiki: %i[edit update preview add_attachment new new_child create] } - - wiki.permission :delete_wiki_pages_attachments, - {} +Rails.application.reloader.to_prepare do + OpenProject::AccessControl.map do |map| + map.project_module nil, order: 100 do + map.permission :add_project, + { projects: %i[new] }, + require: :loggedin, + global: true, + contract_actions: { projects: %i[create] } + + map.permission :create_backup, + { backups: %i[index] }, + require: :loggedin, + global: true, + enabled: -> { OpenProject::Configuration.backup_enabled? } + + map.permission :manage_user, + { + users: %i[index show new create edit update resend_invitation], + 'users/memberships': %i[create update destroy], + admin: %i[index] + }, + require: :loggedin, + global: true, + contract_actions: { users: %i[create read update] } + + map.permission :manage_placeholder_user, + { + placeholder_users: %i[index show new create edit update deletion_info destroy], + 'placeholder_users/memberships': %i[create update destroy], + admin: %i[index] + }, + require: :loggedin, + global: true, + contract_actions: { placeholder_users: %i[create read update] } + + map.permission :view_project, + { projects: [:show], + activities: [:index] }, + public: true - wiki.permission :protect_wiki_pages, - { wiki: :protect }, - require: :member - end + map.permission :search_project, + { search: :index }, + public: true - map.project_module :repository do |repo| - repo.permission :browse_repository, - { repositories: %i[show browse entry annotate changes diff stats graph] } + map.permission :edit_project, + { + 'projects/settings/general': %i[show], + 'projects/settings/storage': %i[show], + 'projects/templated': %i[create destroy], + 'projects/identifier': %i[show update] + }, + require: :member, + contract_actions: { projects: %i[update] } + + map.permission :select_project_modules, + { + 'projects/settings/modules': %i[show update] + }, + require: :member - repo.permission :commit_access, - {} + map.permission :manage_members, + { members: %i[index new create update destroy autocomplete_for_member] }, + require: :member, + dependencies: :view_members, + contract_actions: { members: %i[create update destroy] } + + map.permission :view_members, + { members: [:index] }, + contract_actions: { members: %i[read] } + + map.permission :manage_versions, + { + 'projects/settings/versions': [:show], + versions: %i[new create edit update close_completed destroy] + }, + require: :member - repo.permission :manage_repository, - { - repositories: %i[edit create update committers destroy_info destroy], - 'projects/settings/repository': :show - }, - require: :member + map.permission :manage_types, + { + 'projects/settings/types': %i[show update] + }, + require: :member - repo.permission :view_changesets, - { repositories: %i[show revisions revision] } + map.permission :select_custom_fields, + { + 'projects/settings/custom_fields': %i[show update] + }, + require: :member - repo.permission :view_commit_author_statistics, - {} - end + map.permission :add_subprojects, + { projects: %i[new] }, + require: :member - map.project_module :forums do |forum| - forum.permission :manage_forums, - { forums: %i[new create edit update move destroy] }, + map.permission :copy_projects, + { + projects: %i[copy] + }, + require: :member, + contract_actions: { projects: %i[copy] } + end + + map.project_module :work_package_tracking, order: 90 do |wpt| + wpt.permission :view_work_packages, + { + versions: %i[index show status_by], + journals: %i[index diff], + work_packages: %i[show index], + work_packages_api: [:get], + 'work_packages/reports': %i[report report_details] + }, + contract_actions: { work_packages: %i[read] } + + wpt.permission :add_work_packages, + {} + + wpt.permission :edit_work_packages, + { + 'work_packages/bulk': %i[edit update] + }, + require: :member, + dependencies: :view_work_packages + + wpt.permission :move_work_packages, + { 'work_packages/moves': %i[new create] }, + require: :loggedin, + dependencies: :view_work_packages + + wpt.permission :add_work_package_notes, + { + # FIXME: Although the endpoint is removed, the code checking whether a user + # is eligible to add work packages through the API still seems to rely on this. + journals: [:new] + }, + dependencies: :view_work_packages + + wpt.permission :edit_work_package_notes, + {}, + require: :loggedin, + dependencies: :view_work_packages + + wpt.permission :edit_own_work_package_notes, + {}, + require: :loggedin, + dependencies: :view_work_packages + + # WorkPackage categories + wpt.permission :manage_categories, + { + 'projects/settings/categories': [:show], + categories: %i[new create edit update destroy] + }, require: :member - forum.permission :view_messages, - { forums: %i[index show], - messages: [:show] }, - public: true + wpt.permission :export_work_packages, + { + work_packages: %i[index all] + }, + dependencies: :view_work_packages + + wpt.permission :delete_work_packages, + { + work_packages: :destroy, + 'work_packages/bulk': :destroy + }, + require: :member, + dependencies: :view_work_packages + + wpt.permission :manage_work_package_relations, + { + work_package_relations: %i[create destroy] + }, + dependencies: :view_work_packages + + wpt.permission :manage_subtasks, + {}, + dependencies: :view_work_packages + # Queries + wpt.permission :manage_public_queries, + {}, + require: :member - forum.permission :add_messages, - { messages: %i[new create reply quote preview] } + wpt.permission :save_queries, + {}, + require: :loggedin, + dependencies: :view_work_packages + # Watchers + wpt.permission :view_work_package_watchers, + {}, + dependencies: :view_work_packages + + wpt.permission :add_work_package_watchers, + {}, + dependencies: :view_work_packages + + wpt.permission :delete_work_package_watchers, + {}, + dependencies: :view_work_packages + + wpt.permission :assign_versions, + {}, + dependencies: :view_work_packages + + # A user having the following permission can become assignee and/or responsible of a work package. + # This is a passive permission in the sense that a user having the permission isn't eligible to perform + # actions but rather to have actions taken together with him/her. + wpt.permission :work_package_assigned, + {}, + require: :member, + contract_actions: { work_packages: %i[assigned] }, + grant_to_admin: false + end + + map.project_module :news do |news| + news.permission :view_news, + { news: %i[index show] }, + public: true + + news.permission :manage_news, + { + news: %i[new create edit update destroy preview], + 'news/comments': [:destroy] + }, + require: :member + + news.permission :comment_news, + { 'news/comments': :create } + end + + map.project_module :wiki do |wiki| + wiki.permission :view_wiki_pages, + { wiki: %i[index show special date_index] } + + wiki.permission :list_attachments, + { wiki: :list_attachments }, + require: :member + + wiki.permission :manage_wiki, + { wikis: %i[edit destroy] }, + require: :member + + wiki.permission :manage_wiki_menu, + { wiki_menu_items: %i[edit update select_main_menu_item replace_main_menu_item] }, + require: :member + + wiki.permission :rename_wiki_pages, + { wiki: :rename }, + require: :member + + wiki.permission :change_wiki_parent_page, + { wiki: %i[edit_parent_page update_parent_page] }, + require: :member + + wiki.permission :delete_wiki_pages, + { wiki: :destroy }, + require: :member + + wiki.permission :export_wiki_pages, + { wiki: [:export] } + + wiki.permission :view_wiki_edits, + { wiki: %i[history diff annotate] } + + wiki.permission :edit_wiki_pages, + { wiki: %i[edit update preview add_attachment new new_child create] } + + wiki.permission :delete_wiki_pages_attachments, + {} + + wiki.permission :protect_wiki_pages, + { wiki: :protect }, + require: :member + end + + map.project_module :repository do |repo| + repo.permission :browse_repository, + { repositories: %i[show browse entry annotate changes diff stats graph] } + + repo.permission :commit_access, + {} + + repo.permission :manage_repository, + { + repositories: %i[edit create update committers destroy_info destroy], + 'projects/settings/repository': :show + }, + require: :member + + repo.permission :view_changesets, + { repositories: %i[show revisions revision] } + + repo.permission :view_commit_author_statistics, + {} + end + + map.project_module :forums do |forum| + forum.permission :manage_forums, + { forums: %i[new create edit update move destroy] }, + require: :member + + forum.permission :view_messages, + { forums: %i[index show], + messages: [:show] }, + public: true + + forum.permission :add_messages, + { messages: %i[new create reply quote preview] } + + forum.permission :edit_messages, + { messages: %i[edit update preview] }, + require: :member - forum.permission :edit_messages, - { messages: %i[edit update preview] }, - require: :member + forum.permission :edit_own_messages, + { messages: %i[edit update preview] }, + require: :loggedin - forum.permission :edit_own_messages, - { messages: %i[edit update preview] }, - require: :loggedin + forum.permission :delete_messages, + { messages: :destroy }, + require: :member - forum.permission :delete_messages, - { messages: :destroy }, - require: :member + forum.permission :delete_own_messages, + { messages: :destroy }, + require: :loggedin + end - forum.permission :delete_own_messages, - { messages: :destroy }, - require: :loggedin + map.project_module :activity end - - map.project_module :activity end diff --git a/lib/redmine/plugin.rb b/lib/redmine/plugin.rb index 1d9860d3f2..f4a51e74f8 100644 --- a/lib/redmine/plugin.rb +++ b/lib/redmine/plugin.rb @@ -331,7 +331,10 @@ module Redmine #:nodoc: # end def project_module(name, options = {}, &block) @project_scope = [name, options] - instance_eval(&block) + plugin = self + Rails.application.reloader.to_prepare do + plugin.instance_eval(&block) + end ensure @project_scope = nil end diff --git a/modules/backlogs/lib/open_project/backlogs/engine.rb b/modules/backlogs/lib/open_project/backlogs/engine.rb index 67b164c788..0484f7a2ff 100644 --- a/modules/backlogs/lib/open_project/backlogs/engine.rb +++ b/modules/backlogs/lib/open_project/backlogs/engine.rb @@ -51,16 +51,19 @@ module OpenProject::Backlogs author_url: 'https://www.openproject.org', bundled: true, settings: settings do - OpenProject::AccessControl.permission(:add_work_packages).tap do |add| - add.controller_actions << 'rb_stories/create' - add.controller_actions << 'rb_tasks/create' - add.controller_actions << 'rb_impediments/create' - end - OpenProject::AccessControl.permission(:edit_work_packages).tap do |edit| - edit.controller_actions << 'rb_stories/update' - edit.controller_actions << 'rb_tasks/update' - edit.controller_actions << 'rb_impediments/update' + Rails.application.reloader.to_prepare do + OpenProject::AccessControl.permission(:add_work_packages).tap do |add| + add.controller_actions << 'rb_stories/create' + add.controller_actions << 'rb_tasks/create' + add.controller_actions << 'rb_impediments/create' + end + + OpenProject::AccessControl.permission(:edit_work_packages).tap do |edit| + edit.controller_actions << 'rb_stories/update' + edit.controller_actions << 'rb_tasks/update' + edit.controller_actions << 'rb_impediments/update' + end end project_module :backlogs, dependencies: :work_package_tracking do diff --git a/modules/bim/lib/open_project/bim/engine.rb b/modules/bim/lib/open_project/bim/engine.rb index 3692724f5a..b6944aa87a 100644 --- a/modules/bim/lib/open_project/bim/engine.rb +++ b/modules/bim/lib/open_project/bim/engine.rb @@ -81,7 +81,11 @@ module OpenProject::Bim dependencies: %i[manage_public_queries save_bcf_queries] end - OpenProject::AccessControl.permission(:view_work_packages).controller_actions << 'bim/bcf/issues/redirect_to_bcf_issues_list' + Rails.application.reloader.to_prepare do + OpenProject::AccessControl + .permission(:view_work_packages) + .controller_actions << 'bim/bcf/issues/redirect_to_bcf_issues_list' + end ::Redmine::MenuManager.map(:project_menu) do |menu| menu.push(:ifc_models, diff --git a/modules/dashboards/lib/dashboards/engine.rb b/modules/dashboards/lib/dashboards/engine.rb index cebe8b212f..e7a3a9168c 100644 --- a/modules/dashboards/lib/dashboards/engine.rb +++ b/modules/dashboards/lib/dashboards/engine.rb @@ -20,13 +20,15 @@ module Dashboards end initializer 'dashboards.permissions' do - # deactivate for now - next unless Rails.env == 'test' - - OpenProject::AccessControl.map do |ac_map| - ac_map.project_module(:dashboards) do |pm_map| - pm_map.permission(:view_dashboards, { 'dashboards/dashboards': ['show'] }) - pm_map.permission(:manage_dashboards, { 'dashboards/dashboards': ['show'] }) + Rails.application.reloader.to_prepare do + # deactivate for now + next unless Rails.env.test? + + OpenProject::AccessControl.map do |ac_map| + ac_map.project_module(:dashboards) do |pm_map| + pm_map.permission(:view_dashboards, { 'dashboards/dashboards': ['show'] }) + pm_map.permission(:manage_dashboards, { 'dashboards/dashboards': ['show'] }) + end end end end diff --git a/modules/github_integration/lib/open_project/github_integration/engine.rb b/modules/github_integration/lib/open_project/github_integration/engine.rb index 66919d7387..3bfc7466f3 100644 --- a/modules/github_integration/lib/open_project/github_integration/engine.rb +++ b/modules/github_integration/lib/open_project/github_integration/engine.rb @@ -41,7 +41,11 @@ module OpenProject::GithubIntegration register 'openproject-github_integration', author_url: 'https://www.openproject.org/', - bundled: true + bundled: true do + project_module(:github, dependencies: :work_package_tracking) do + permission(:show_github_content, {}) + end + end initializer 'github.register_hook' do ::OpenProject::Webhooks.register_hook 'github' do |hook, environment, params, user| @@ -58,14 +62,6 @@ module OpenProject::GithubIntegration &NotificationHandler.method(:pull_request)) end - initializer 'github.permissions' do - OpenProject::AccessControl.map do |ac_map| - ac_map.project_module(:github, dependencies: :work_package_tracking) do |pm_map| - pm_map.permission(:show_github_content, {}) - end - end - end - extend_api_response(:v3, :work_packages, :work_package, &::OpenProject::GithubIntegration::Patches::API::WorkPackageRepresenter.extension) diff --git a/modules/overviews/lib/overviews/engine.rb b/modules/overviews/lib/overviews/engine.rb index bfc4c6a7ec..851bbef8a2 100644 --- a/modules/overviews/lib/overviews/engine.rb +++ b/modules/overviews/lib/overviews/engine.rb @@ -15,15 +15,17 @@ module Overviews end initializer 'overviews.permissions' do - OpenProject::AccessControl.permission(:view_project) - .controller_actions - .push('overviews/overviews/show') - - OpenProject::AccessControl.map do |ac_map| - ac_map.project_module nil do |map| - map.permission :manage_overview, - { 'overviews/overviews': ['show'] }, - public: true + Rails.application.reloader.to_prepare do + OpenProject::AccessControl.permission(:view_project) + .controller_actions + .push('overviews/overviews/show') + + OpenProject::AccessControl.map do |ac_map| + ac_map.project_module nil do |map| + map.permission :manage_overview, + { 'overviews/overviews': ['show'] }, + public: true + end end end end diff --git a/modules/reporting/lib/open_project/reporting/engine.rb b/modules/reporting/lib/open_project/reporting/engine.rb index 5aca96ceaf..fa44b8adc7 100644 --- a/modules/reporting/lib/open_project/reporting/engine.rb +++ b/modules/reporting/lib/open_project/reporting/engine.rb @@ -46,12 +46,16 @@ module OpenProject::Reporting permission :save_private_cost_reports, { cost_reports: edit_actions } end - # register additional permissions for viewing time and cost entries through the CostReportsController - view_actions.each do |action| - OpenProject::AccessControl.permission(:view_time_entries).controller_actions << "cost_reports/#{action}" - OpenProject::AccessControl.permission(:view_own_time_entries).controller_actions << "cost_reports/#{action}" - OpenProject::AccessControl.permission(:view_cost_entries).controller_actions << "cost_reports/#{action}" - OpenProject::AccessControl.permission(:view_own_cost_entries).controller_actions << "cost_reports/#{action}" + Rails.application.reloader.to_prepare do + OpenProject::AccessControl.map do + # register additional permissions for viewing time and cost entries through the CostReportsController + view_actions.each do |action| + OpenProject::AccessControl.permission(:view_time_entries).controller_actions << "cost_reports/#{action}" + OpenProject::AccessControl.permission(:view_own_time_entries).controller_actions << "cost_reports/#{action}" + OpenProject::AccessControl.permission(:view_cost_entries).controller_actions << "cost_reports/#{action}" + OpenProject::AccessControl.permission(:view_own_cost_entries).controller_actions << "cost_reports/#{action}" + end + end end # menu extensions From 92d00affca0f809d2536cd8a5dfe86fbe5e9b342 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 4 Apr 2022 15:45:29 +0200 Subject: [PATCH 05/80] use active_support hook for patching --- app/helpers/avatar_helper.rb | 2 ++ modules/avatars/lib/open_project/avatars/engine.rb | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/helpers/avatar_helper.rb b/app/helpers/avatar_helper.rb index 40dadf9c3a..3136dbe3e6 100644 --- a/app/helpers/avatar_helper.rb +++ b/app/helpers/avatar_helper.rb @@ -38,3 +38,5 @@ module AvatarHelper ''.html_safe end end + +ActiveSupport.run_load_hooks(:op_helpers_avatar, AvatarHelper) diff --git a/modules/avatars/lib/open_project/avatars/engine.rb b/modules/avatars/lib/open_project/avatars/engine.rb index f81701d551..fe034de716 100644 --- a/modules/avatars/lib/open_project/avatars/engine.rb +++ b/modules/avatars/lib/open_project/avatars/engine.rb @@ -53,12 +53,11 @@ module OpenProject::Avatars only_if: ->(*) { User.current.admin? && ::OpenProject::Avatars::AvatarManager.avatars_enabled? } initializer 'patch avatar helper' do - # This is required to be an initializer, - # since the helpers are included as soon as the ApplicationController - # gets autoloaded, which is BEFORE config.to_prepare. Rails.autoloaders.main.ignore(config.root.join('lib/open_project/avatars/patches/avatar_helper_patch.rb')) - require_relative './patches/avatar_helper_patch' + ActiveSupport.on_load(:op_helpers_avatar) do + require_relative './patches/avatar_helper_patch' + end end patches %i[User] From b5c76e3027937974f2d9e3dc863fb998c7ff168b Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 4 Apr 2022 16:03:18 +0200 Subject: [PATCH 06/80] introduce lib_static --- .../{00-core_plugins.rb => 00-load_lib_static.rb} | 15 ++++++++++----- config/initializers/zeitwerk.rb | 1 - lib/tasks/code.rake | 4 ++-- lib/tasks/copyright.rake | 2 +- lib/tasks/parallel_testing.rake | 1 - .../plugins/acts_as_attachable/init.rb | 0 .../acts_as_attachable/lib/acts_as_attachable.rb | 0 .../plugins/acts_as_customizable/init.rb | 0 .../lib/acts_as_customizable.rb | 0 .../lib/human_attribute_name.rb | 0 {lib => lib_static}/plugins/acts_as_event/init.rb | 0 .../plugins/acts_as_event/lib/acts_as_event.rb | 0 .../plugins/acts_as_journalized/GPL.txt | 0 .../plugins/acts_as_journalized/LICENSE | 0 .../plugins/acts_as_journalized/README.rdoc | 0 .../plugins/acts_as_journalized/REVISION | 0 .../plugins/acts_as_journalized/init.rb | 0 .../lib/acts/journalized/creation.rb | 0 .../lib/acts/journalized/data_class.rb | 0 .../lib/acts/journalized/format_hooks.rb | 0 .../lib/acts/journalized/journal_object_cache.rb | 0 .../lib/acts/journalized/journalized.rb | 0 .../lib/acts/journalized/options.rb | 0 .../lib/acts/journalized/permissions.rb | 0 .../lib/acts/journalized/reversion.rb | 0 .../lib/acts/journalized/save_hooks.rb | 0 .../lib/acts_as_journalized.rb | 0 .../acts_as_journalized/lib/journal_changes.rb | 0 .../acts_as_journalized/lib/journal_deprecated.rb | 0 .../acts_as_journalized/lib/journal_detail.rb | 0 .../acts_as_journalized/lib/journal_formatter.rb | 0 .../lib/journal_formatter/attribute.rb | 0 .../lib/journal_formatter/base.rb | 0 .../lib/journal_formatter/datetime.rb | 0 .../lib/journal_formatter/decimal.rb | 0 .../lib/journal_formatter/fraction.rb | 0 .../lib/journal_formatter/id.rb | 0 .../lib/journal_formatter/named_association.rb | 0 .../lib/journal_formatter/plaintext.rb | 0 .../lib/journal_formatter/proc.rb | 0 .../plugins/acts_as_searchable/init.rb | 0 .../acts_as_searchable/lib/acts_as_searchable.rb | 0 .../plugins/acts_as_watchable/init.rb | 0 .../acts_as_watchable/lib/acts_as_watchable.rb | 0 .../lib/acts_as_watchable/routes.rb | 0 {lib => lib_static}/plugins/load_path_helper.rb | 0 .../plugins/verification/MIT-LICENSE | 0 {lib => lib_static}/plugins/verification/README | 0 {lib => lib_static}/plugins/verification/Rakefile | 0 {lib => lib_static}/plugins/verification/init.rb | 0 .../lib/action_controller/verification.rb | 0 {constants => lib_static}/redmine/diff.rb | 0 .../redmine/diff/array_string_diff.rb | 0 .../redmine/diff/diffable.rb | 0 {constants => lib_static}/redmine/i18n.rb | 0 55 files changed, 13 insertions(+), 10 deletions(-) rename config/initializers/{00-core_plugins.rb => 00-load_lib_static.rb} (74%) rename {lib => lib_static}/plugins/acts_as_attachable/init.rb (100%) rename {lib => lib_static}/plugins/acts_as_attachable/lib/acts_as_attachable.rb (100%) rename {lib => lib_static}/plugins/acts_as_customizable/init.rb (100%) rename {lib => lib_static}/plugins/acts_as_customizable/lib/acts_as_customizable.rb (100%) rename {lib => lib_static}/plugins/acts_as_customizable/lib/human_attribute_name.rb (100%) rename {lib => lib_static}/plugins/acts_as_event/init.rb (100%) rename {lib => lib_static}/plugins/acts_as_event/lib/acts_as_event.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/GPL.txt (100%) rename {lib => lib_static}/plugins/acts_as_journalized/LICENSE (100%) rename {lib => lib_static}/plugins/acts_as_journalized/README.rdoc (100%) rename {lib => lib_static}/plugins/acts_as_journalized/REVISION (100%) rename {lib => lib_static}/plugins/acts_as_journalized/init.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/acts/journalized/creation.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/acts/journalized/data_class.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/acts/journalized/format_hooks.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/acts/journalized/journal_object_cache.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/acts/journalized/journalized.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/acts/journalized/options.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/acts/journalized/permissions.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/acts/journalized/reversion.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/acts/journalized/save_hooks.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/acts_as_journalized.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/journal_changes.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/journal_deprecated.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/journal_detail.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/journal_formatter.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/journal_formatter/attribute.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/journal_formatter/base.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/journal_formatter/datetime.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/journal_formatter/decimal.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/journal_formatter/fraction.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/journal_formatter/id.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/journal_formatter/named_association.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/journal_formatter/plaintext.rb (100%) rename {lib => lib_static}/plugins/acts_as_journalized/lib/journal_formatter/proc.rb (100%) rename {lib => lib_static}/plugins/acts_as_searchable/init.rb (100%) rename {lib => lib_static}/plugins/acts_as_searchable/lib/acts_as_searchable.rb (100%) rename {lib => lib_static}/plugins/acts_as_watchable/init.rb (100%) rename {lib => lib_static}/plugins/acts_as_watchable/lib/acts_as_watchable.rb (100%) rename {lib => lib_static}/plugins/acts_as_watchable/lib/acts_as_watchable/routes.rb (100%) rename {lib => lib_static}/plugins/load_path_helper.rb (100%) rename {lib => lib_static}/plugins/verification/MIT-LICENSE (100%) rename {lib => lib_static}/plugins/verification/README (100%) rename {lib => lib_static}/plugins/verification/Rakefile (100%) rename {lib => lib_static}/plugins/verification/init.rb (100%) rename {lib => lib_static}/plugins/verification/lib/action_controller/verification.rb (100%) rename {constants => lib_static}/redmine/diff.rb (100%) rename {constants => lib_static}/redmine/diff/array_string_diff.rb (100%) rename {constants => lib_static}/redmine/diff/diffable.rb (100%) rename {constants => lib_static}/redmine/i18n.rb (100%) diff --git a/config/initializers/00-core_plugins.rb b/config/initializers/00-load_lib_static.rb similarity index 74% rename from config/initializers/00-core_plugins.rb rename to config/initializers/00-load_lib_static.rb index b11d42c74d..9064ab301d 100644 --- a/config/initializers/00-core_plugins.rb +++ b/config/initializers/00-load_lib_static.rb @@ -26,12 +26,17 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require Rails.root.join('constants/redmine/i18n') -require Rails.root.join('constants/redmine/diff') -require Rails.root.join('constants/redmine/diff/diffable') +# TODO: check if this can be postponed and if some plugins can make use of the ActiveSupport.on_load hooks -# Loads the core plugins located in lib/plugins -Dir.glob(File.join(Rails.root, 'lib/plugins/*')).sort.each do |directory| +require Rails.root.join('lib_static/redmine/i18n') +require Rails.root.join('lib_static/redmine/diff') +require Rails.root.join('lib_static/redmine/diff/diffable') +require Rails.root.join('lib_static/redmine/diff/array_string_diff') + +require Rails.root.join('lib_static/plugins/load_path_helper') + +# Loads the core plugins located in lib_static/plugins +Dir.glob(Rails.root.join('lib_static/plugins/*')).each do |directory| if File.directory?(directory) lib = File.join(directory, 'lib') diff --git a/config/initializers/zeitwerk.rb b/config/initializers/zeitwerk.rb index 27c91236e3..e0b8ce4d8f 100644 --- a/config/initializers/zeitwerk.rb +++ b/config/initializers/zeitwerk.rb @@ -68,7 +68,6 @@ Rails.autoloaders.each do |autoloader| autoloader.inflector = OpenProject::Inflector.new(__FILE__) end -Rails.autoloaders.main.ignore(Rails.root.join('lib/plugins')) Rails.autoloaders.main.ignore(Rails.root.join('lib/open_project/patches')) Rails.autoloaders.main.ignore(Rails.root.join('lib/generators')) diff --git a/lib/tasks/code.rake b/lib/tasks/code.rake index 2048026011..71308a9d79 100644 --- a/lib/tasks/code.rake +++ b/lib/tasks/code.rake @@ -32,8 +32,8 @@ namespace :code do Dir.chdir(File.join(File.dirname(__FILE__), '../..')) do files = Dir['**/**{.rb,.html.erb,.rhtml,.rjs,.plain.erb,.rxml,.yml,.rake,.eml}'] files.reject! do |f| - f.include?('lib/plugins') || - f.include?('lib/diff') + f.include?('lib_static/plugins') || + f.include?('lib_static/diff') end # handle files in chunks of 50 to avoid too long command lines diff --git a/lib/tasks/copyright.rake b/lib/tasks/copyright.rake index a5b8e5b2d4..d9788fa7e4 100644 --- a/lib/tasks/copyright.rake +++ b/lib/tasks/copyright.rake @@ -143,7 +143,7 @@ namespace :copyright do desc 'Update the copyright on .rb source files' task :update_rb, :arg1 do |_task, args| - excluded = (%w(acts_as_tree rfpdf verification).map { |dir| "lib/plugins/#{dir}" }) + excluded = (%w(acts_as_tree rfpdf verification).map { |dir| "lib_static/plugins/#{dir}" }) rewrite_copyright('rb', excluded, :rb, args[:arg1]) end diff --git a/lib/tasks/parallel_testing.rake b/lib/tasks/parallel_testing.rake index 9851dfd7e6..3e5ae536d5 100644 --- a/lib/tasks/parallel_testing.rake +++ b/lib/tasks/parallel_testing.rake @@ -27,7 +27,6 @@ #++ require 'optparse' -require 'plugins/load_path_helper' begin Bundler.gem('parallel_tests') diff --git a/lib/plugins/acts_as_attachable/init.rb b/lib_static/plugins/acts_as_attachable/init.rb similarity index 100% rename from lib/plugins/acts_as_attachable/init.rb rename to lib_static/plugins/acts_as_attachable/init.rb diff --git a/lib/plugins/acts_as_attachable/lib/acts_as_attachable.rb b/lib_static/plugins/acts_as_attachable/lib/acts_as_attachable.rb similarity index 100% rename from lib/plugins/acts_as_attachable/lib/acts_as_attachable.rb rename to lib_static/plugins/acts_as_attachable/lib/acts_as_attachable.rb diff --git a/lib/plugins/acts_as_customizable/init.rb b/lib_static/plugins/acts_as_customizable/init.rb similarity index 100% rename from lib/plugins/acts_as_customizable/init.rb rename to lib_static/plugins/acts_as_customizable/init.rb diff --git a/lib/plugins/acts_as_customizable/lib/acts_as_customizable.rb b/lib_static/plugins/acts_as_customizable/lib/acts_as_customizable.rb similarity index 100% rename from lib/plugins/acts_as_customizable/lib/acts_as_customizable.rb rename to lib_static/plugins/acts_as_customizable/lib/acts_as_customizable.rb diff --git a/lib/plugins/acts_as_customizable/lib/human_attribute_name.rb b/lib_static/plugins/acts_as_customizable/lib/human_attribute_name.rb similarity index 100% rename from lib/plugins/acts_as_customizable/lib/human_attribute_name.rb rename to lib_static/plugins/acts_as_customizable/lib/human_attribute_name.rb diff --git a/lib/plugins/acts_as_event/init.rb b/lib_static/plugins/acts_as_event/init.rb similarity index 100% rename from lib/plugins/acts_as_event/init.rb rename to lib_static/plugins/acts_as_event/init.rb diff --git a/lib/plugins/acts_as_event/lib/acts_as_event.rb b/lib_static/plugins/acts_as_event/lib/acts_as_event.rb similarity index 100% rename from lib/plugins/acts_as_event/lib/acts_as_event.rb rename to lib_static/plugins/acts_as_event/lib/acts_as_event.rb diff --git a/lib/plugins/acts_as_journalized/GPL.txt b/lib_static/plugins/acts_as_journalized/GPL.txt similarity index 100% rename from lib/plugins/acts_as_journalized/GPL.txt rename to lib_static/plugins/acts_as_journalized/GPL.txt diff --git a/lib/plugins/acts_as_journalized/LICENSE b/lib_static/plugins/acts_as_journalized/LICENSE similarity index 100% rename from lib/plugins/acts_as_journalized/LICENSE rename to lib_static/plugins/acts_as_journalized/LICENSE diff --git a/lib/plugins/acts_as_journalized/README.rdoc b/lib_static/plugins/acts_as_journalized/README.rdoc similarity index 100% rename from lib/plugins/acts_as_journalized/README.rdoc rename to lib_static/plugins/acts_as_journalized/README.rdoc diff --git a/lib/plugins/acts_as_journalized/REVISION b/lib_static/plugins/acts_as_journalized/REVISION similarity index 100% rename from lib/plugins/acts_as_journalized/REVISION rename to lib_static/plugins/acts_as_journalized/REVISION diff --git a/lib/plugins/acts_as_journalized/init.rb b/lib_static/plugins/acts_as_journalized/init.rb similarity index 100% rename from lib/plugins/acts_as_journalized/init.rb rename to lib_static/plugins/acts_as_journalized/init.rb diff --git a/lib/plugins/acts_as_journalized/lib/acts/journalized/creation.rb b/lib_static/plugins/acts_as_journalized/lib/acts/journalized/creation.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/acts/journalized/creation.rb rename to lib_static/plugins/acts_as_journalized/lib/acts/journalized/creation.rb diff --git a/lib/plugins/acts_as_journalized/lib/acts/journalized/data_class.rb b/lib_static/plugins/acts_as_journalized/lib/acts/journalized/data_class.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/acts/journalized/data_class.rb rename to lib_static/plugins/acts_as_journalized/lib/acts/journalized/data_class.rb diff --git a/lib/plugins/acts_as_journalized/lib/acts/journalized/format_hooks.rb b/lib_static/plugins/acts_as_journalized/lib/acts/journalized/format_hooks.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/acts/journalized/format_hooks.rb rename to lib_static/plugins/acts_as_journalized/lib/acts/journalized/format_hooks.rb diff --git a/lib/plugins/acts_as_journalized/lib/acts/journalized/journal_object_cache.rb b/lib_static/plugins/acts_as_journalized/lib/acts/journalized/journal_object_cache.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/acts/journalized/journal_object_cache.rb rename to lib_static/plugins/acts_as_journalized/lib/acts/journalized/journal_object_cache.rb diff --git a/lib/plugins/acts_as_journalized/lib/acts/journalized/journalized.rb b/lib_static/plugins/acts_as_journalized/lib/acts/journalized/journalized.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/acts/journalized/journalized.rb rename to lib_static/plugins/acts_as_journalized/lib/acts/journalized/journalized.rb diff --git a/lib/plugins/acts_as_journalized/lib/acts/journalized/options.rb b/lib_static/plugins/acts_as_journalized/lib/acts/journalized/options.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/acts/journalized/options.rb rename to lib_static/plugins/acts_as_journalized/lib/acts/journalized/options.rb diff --git a/lib/plugins/acts_as_journalized/lib/acts/journalized/permissions.rb b/lib_static/plugins/acts_as_journalized/lib/acts/journalized/permissions.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/acts/journalized/permissions.rb rename to lib_static/plugins/acts_as_journalized/lib/acts/journalized/permissions.rb diff --git a/lib/plugins/acts_as_journalized/lib/acts/journalized/reversion.rb b/lib_static/plugins/acts_as_journalized/lib/acts/journalized/reversion.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/acts/journalized/reversion.rb rename to lib_static/plugins/acts_as_journalized/lib/acts/journalized/reversion.rb diff --git a/lib/plugins/acts_as_journalized/lib/acts/journalized/save_hooks.rb b/lib_static/plugins/acts_as_journalized/lib/acts/journalized/save_hooks.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/acts/journalized/save_hooks.rb rename to lib_static/plugins/acts_as_journalized/lib/acts/journalized/save_hooks.rb diff --git a/lib/plugins/acts_as_journalized/lib/acts_as_journalized.rb b/lib_static/plugins/acts_as_journalized/lib/acts_as_journalized.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/acts_as_journalized.rb rename to lib_static/plugins/acts_as_journalized/lib/acts_as_journalized.rb diff --git a/lib/plugins/acts_as_journalized/lib/journal_changes.rb b/lib_static/plugins/acts_as_journalized/lib/journal_changes.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/journal_changes.rb rename to lib_static/plugins/acts_as_journalized/lib/journal_changes.rb diff --git a/lib/plugins/acts_as_journalized/lib/journal_deprecated.rb b/lib_static/plugins/acts_as_journalized/lib/journal_deprecated.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/journal_deprecated.rb rename to lib_static/plugins/acts_as_journalized/lib/journal_deprecated.rb diff --git a/lib/plugins/acts_as_journalized/lib/journal_detail.rb b/lib_static/plugins/acts_as_journalized/lib/journal_detail.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/journal_detail.rb rename to lib_static/plugins/acts_as_journalized/lib/journal_detail.rb diff --git a/lib/plugins/acts_as_journalized/lib/journal_formatter.rb b/lib_static/plugins/acts_as_journalized/lib/journal_formatter.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/journal_formatter.rb rename to lib_static/plugins/acts_as_journalized/lib/journal_formatter.rb diff --git a/lib/plugins/acts_as_journalized/lib/journal_formatter/attribute.rb b/lib_static/plugins/acts_as_journalized/lib/journal_formatter/attribute.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/journal_formatter/attribute.rb rename to lib_static/plugins/acts_as_journalized/lib/journal_formatter/attribute.rb diff --git a/lib/plugins/acts_as_journalized/lib/journal_formatter/base.rb b/lib_static/plugins/acts_as_journalized/lib/journal_formatter/base.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/journal_formatter/base.rb rename to lib_static/plugins/acts_as_journalized/lib/journal_formatter/base.rb diff --git a/lib/plugins/acts_as_journalized/lib/journal_formatter/datetime.rb b/lib_static/plugins/acts_as_journalized/lib/journal_formatter/datetime.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/journal_formatter/datetime.rb rename to lib_static/plugins/acts_as_journalized/lib/journal_formatter/datetime.rb diff --git a/lib/plugins/acts_as_journalized/lib/journal_formatter/decimal.rb b/lib_static/plugins/acts_as_journalized/lib/journal_formatter/decimal.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/journal_formatter/decimal.rb rename to lib_static/plugins/acts_as_journalized/lib/journal_formatter/decimal.rb diff --git a/lib/plugins/acts_as_journalized/lib/journal_formatter/fraction.rb b/lib_static/plugins/acts_as_journalized/lib/journal_formatter/fraction.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/journal_formatter/fraction.rb rename to lib_static/plugins/acts_as_journalized/lib/journal_formatter/fraction.rb diff --git a/lib/plugins/acts_as_journalized/lib/journal_formatter/id.rb b/lib_static/plugins/acts_as_journalized/lib/journal_formatter/id.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/journal_formatter/id.rb rename to lib_static/plugins/acts_as_journalized/lib/journal_formatter/id.rb diff --git a/lib/plugins/acts_as_journalized/lib/journal_formatter/named_association.rb b/lib_static/plugins/acts_as_journalized/lib/journal_formatter/named_association.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/journal_formatter/named_association.rb rename to lib_static/plugins/acts_as_journalized/lib/journal_formatter/named_association.rb diff --git a/lib/plugins/acts_as_journalized/lib/journal_formatter/plaintext.rb b/lib_static/plugins/acts_as_journalized/lib/journal_formatter/plaintext.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/journal_formatter/plaintext.rb rename to lib_static/plugins/acts_as_journalized/lib/journal_formatter/plaintext.rb diff --git a/lib/plugins/acts_as_journalized/lib/journal_formatter/proc.rb b/lib_static/plugins/acts_as_journalized/lib/journal_formatter/proc.rb similarity index 100% rename from lib/plugins/acts_as_journalized/lib/journal_formatter/proc.rb rename to lib_static/plugins/acts_as_journalized/lib/journal_formatter/proc.rb diff --git a/lib/plugins/acts_as_searchable/init.rb b/lib_static/plugins/acts_as_searchable/init.rb similarity index 100% rename from lib/plugins/acts_as_searchable/init.rb rename to lib_static/plugins/acts_as_searchable/init.rb diff --git a/lib/plugins/acts_as_searchable/lib/acts_as_searchable.rb b/lib_static/plugins/acts_as_searchable/lib/acts_as_searchable.rb similarity index 100% rename from lib/plugins/acts_as_searchable/lib/acts_as_searchable.rb rename to lib_static/plugins/acts_as_searchable/lib/acts_as_searchable.rb diff --git a/lib/plugins/acts_as_watchable/init.rb b/lib_static/plugins/acts_as_watchable/init.rb similarity index 100% rename from lib/plugins/acts_as_watchable/init.rb rename to lib_static/plugins/acts_as_watchable/init.rb diff --git a/lib/plugins/acts_as_watchable/lib/acts_as_watchable.rb b/lib_static/plugins/acts_as_watchable/lib/acts_as_watchable.rb similarity index 100% rename from lib/plugins/acts_as_watchable/lib/acts_as_watchable.rb rename to lib_static/plugins/acts_as_watchable/lib/acts_as_watchable.rb diff --git a/lib/plugins/acts_as_watchable/lib/acts_as_watchable/routes.rb b/lib_static/plugins/acts_as_watchable/lib/acts_as_watchable/routes.rb similarity index 100% rename from lib/plugins/acts_as_watchable/lib/acts_as_watchable/routes.rb rename to lib_static/plugins/acts_as_watchable/lib/acts_as_watchable/routes.rb diff --git a/lib/plugins/load_path_helper.rb b/lib_static/plugins/load_path_helper.rb similarity index 100% rename from lib/plugins/load_path_helper.rb rename to lib_static/plugins/load_path_helper.rb diff --git a/lib/plugins/verification/MIT-LICENSE b/lib_static/plugins/verification/MIT-LICENSE similarity index 100% rename from lib/plugins/verification/MIT-LICENSE rename to lib_static/plugins/verification/MIT-LICENSE diff --git a/lib/plugins/verification/README b/lib_static/plugins/verification/README similarity index 100% rename from lib/plugins/verification/README rename to lib_static/plugins/verification/README diff --git a/lib/plugins/verification/Rakefile b/lib_static/plugins/verification/Rakefile similarity index 100% rename from lib/plugins/verification/Rakefile rename to lib_static/plugins/verification/Rakefile diff --git a/lib/plugins/verification/init.rb b/lib_static/plugins/verification/init.rb similarity index 100% rename from lib/plugins/verification/init.rb rename to lib_static/plugins/verification/init.rb diff --git a/lib/plugins/verification/lib/action_controller/verification.rb b/lib_static/plugins/verification/lib/action_controller/verification.rb similarity index 100% rename from lib/plugins/verification/lib/action_controller/verification.rb rename to lib_static/plugins/verification/lib/action_controller/verification.rb diff --git a/constants/redmine/diff.rb b/lib_static/redmine/diff.rb similarity index 100% rename from constants/redmine/diff.rb rename to lib_static/redmine/diff.rb diff --git a/constants/redmine/diff/array_string_diff.rb b/lib_static/redmine/diff/array_string_diff.rb similarity index 100% rename from constants/redmine/diff/array_string_diff.rb rename to lib_static/redmine/diff/array_string_diff.rb diff --git a/constants/redmine/diff/diffable.rb b/lib_static/redmine/diff/diffable.rb similarity index 100% rename from constants/redmine/diff/diffable.rb rename to lib_static/redmine/diff/diffable.rb diff --git a/constants/redmine/i18n.rb b/lib_static/redmine/i18n.rb similarity index 100% rename from constants/redmine/i18n.rb rename to lib_static/redmine/i18n.rb From 0dfd30ca56d60f9f3cd80cdb9b81f23e2bf9a56a Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 4 Apr 2022 16:29:32 +0200 Subject: [PATCH 07/80] defer configuring session store --- config/initializers/session_store.rb | 92 ++++++++++++++-------------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 8c416a1be1..d035328f0b 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -66,56 +66,58 @@ module OpenProject end end -config = OpenProject::Configuration +Rails.application.config.after_initialize do + config = OpenProject::Configuration -# Enforce session storage for testing -if Rails.env.test? - config['session_store'] = :active_record_store -end + # Enforce session storage for testing + if Rails.env.test? + config['session_store'] = :active_record_store + end -session_store = config['session_store'].to_sym -relative_url_root = config['rails_relative_url_root'].presence - -session_options = { - key: config['session_cookie_name'], - httponly: true, - secure: Setting.https?, - path: relative_url_root -} - -if session_store == :cache_store - # env OPENPROJECT_CACHE__STORE__SESSION__USER__TTL__DAYS - session_ttl = config['cache_store_session_user_ttl_days']&.to_i&.days || 3.days - - # Extend session cache entry TTL so that they can stay logged in when their - # session ID cookie's TTL is 'session' where usually the session entry in the - # cache would expire before the session in the browser by default. - session_options[:expire_store_after] = lambda do |session, expire_after| - if session.include? "user_id" # logged-in user - [session_ttl, expire_after].compact.max - else - expire_after # anonymous user + session_store = config['session_store'].to_sym + relative_url_root = config['rails_relative_url_root'].presence + + session_options = { + key: config['session_cookie_name'], + httponly: true, + secure: Setting.https?, + path: relative_url_root + } + + if session_store == :cache_store + # env OPENPROJECT_CACHE__STORE__SESSION__USER__TTL__DAYS + session_ttl = config['cache_store_session_user_ttl_days']&.to_i&.days || 3.days + + # Extend session cache entry TTL so that they can stay logged in when their + # session ID cookie's TTL is 'session' where usually the session entry in the + # cache would expire before the session in the browser by default. + session_options[:expire_store_after] = lambda do |session, expire_after| + if session.include? "user_id" # logged-in user + [session_ttl, expire_after].compact.max + else + expire_after # anonymous user + end end - end - method = ActionDispatch::Session::CacheStore.instance_method(:write_session) - unless method.to_s.include?("write_session(env, sid, session, options)") - raise( - "The signature for `ActionDispatch::Session::CacheStore.write_session` " + - "seems to have changed. Please update the " + - "`ExpireStoreAfterOption` module (and this check) in #{__FILE__}" - ) - end + method = ActionDispatch::Session::CacheStore.instance_method(:write_session) + unless method.to_s.include?("write_session(env, sid, session, options)") + raise( + "The signature for `ActionDispatch::Session::CacheStore.write_session` " + + "seems to have changed. Please update the " + + "`ExpireStoreAfterOption` module (and this check) in #{__FILE__}" + ) + end - ActionDispatch::Session::CacheStore.prepend OpenProject::ExpireStoreAfterOption -end + ActionDispatch::Session::CacheStore.prepend OpenProject::ExpireStoreAfterOption + end -OpenProject::Application.config.session_store session_store, **session_options + OpenProject::Application.config.session_store session_store, **session_options -## -# We use our own decorated session model to note the user_id -# for each session. -ActionDispatch::Session::ActiveRecordStore.session_class = ::Sessions::SqlBypass -# Continue to use marshal serialization to retain symbols and whatnot -ActiveRecord::SessionStore::Session.serializer = :marshal + ## + # We use our own decorated session model to note the user_id + # for each session. + ActionDispatch::Session::ActiveRecordStore.session_class = ::Sessions::SqlBypass + # Continue to use marshal serialization to retain symbols and whatnot + ActiveRecord::SessionStore::Session.serializer = :marshal +end From 0815cb5f7d77740ab83eeb53919f3977948432d0 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 4 Apr 2022 16:45:27 +0200 Subject: [PATCH 08/80] defer registering for op notification --- config/initializers/user_invitation.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/config/initializers/user_invitation.rb b/config/initializers/user_invitation.rb index 3504f5c71b..5b043ec329 100644 --- a/config/initializers/user_invitation.rb +++ b/config/initializers/user_invitation.rb @@ -1,10 +1,12 @@ -## -# The default behaviour is to send the user a sign-up mail -# when they were invited. -OpenProject::Notifications.subscribe UserInvitation::Events.user_invited do |token| - Mails::InvitationJob.perform_later(token) -end +Rails.application.config.after_initialize do + ## + # The default behaviour is to send the user a sign-up mail + # when they were invited. + OpenProject::Notifications.subscribe UserInvitation::Events.user_invited do |token| + Mails::InvitationJob.perform_later(token) + end -OpenProject::Notifications.subscribe UserInvitation::Events.user_reinvited do |token| - Mails::InvitationJob.perform_later(token) + OpenProject::Notifications.subscribe UserInvitation::Events.user_reinvited do |token| + Mails::InvitationJob.perform_later(token) + end end From 3492d08fdd4620d3ab2a579f99d2dd7a114b5fbb Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 4 Apr 2022 17:08:23 +0200 Subject: [PATCH 09/80] turn OpenProject::Notifications static --- config/initializers/00-load_lib_static.rb | 1 + config/initializers/subscribe_listeners.rb | 92 ++++++++++--------- .../open_project/notifications.rb | 0 .../open_project/github_integration/engine.rb | 16 ++-- .../lib/open_project/webhooks/engine.rb | 6 +- 5 files changed, 61 insertions(+), 54 deletions(-) rename {lib => lib_static}/open_project/notifications.rb (100%) diff --git a/config/initializers/00-load_lib_static.rb b/config/initializers/00-load_lib_static.rb index 9064ab301d..ebf63d5458 100644 --- a/config/initializers/00-load_lib_static.rb +++ b/config/initializers/00-load_lib_static.rb @@ -34,6 +34,7 @@ require Rails.root.join('lib_static/redmine/diff/diffable') require Rails.root.join('lib_static/redmine/diff/array_string_diff') require Rails.root.join('lib_static/plugins/load_path_helper') +require Rails.root.join('lib_static/open_project/notifications') # Loads the core plugins located in lib_static/plugins Dir.glob(Rails.root.join('lib_static/plugins/*')).each do |directory| diff --git a/config/initializers/subscribe_listeners.rb b/config/initializers/subscribe_listeners.rb index 2d75328f0f..8b6f39138f 100644 --- a/config/initializers/subscribe_listeners.rb +++ b/config/initializers/subscribe_listeners.rb @@ -26,59 +26,61 @@ # See COPYRIGHT and LICENSE files for more details. #++ -OpenProject::Notifications.subscribe(OpenProject::Events::JOURNAL_CREATED) do |payload| - # A job is scheduled that creates notifications (in app if supported) right away and schedules - # jobs to be run for mail and digest mails. - Notifications::WorkflowJob - .perform_later(:create_notifications, - payload[:journal], - payload[:send_notification]) +Rails.application.config.after_initialize do + OpenProject::Notifications.subscribe(OpenProject::Events::JOURNAL_CREATED) do |payload| + # A job is scheduled that creates notifications (in app if supported) right away and schedules + # jobs to be run for mail and digest mails. + Notifications::WorkflowJob + .perform_later(:create_notifications, + payload[:journal], + payload[:send_notification]) - # A job is scheduled for the end of the journal aggregation time. If the journal does still exist - # at the end (it might be replaced because another journal was created within that timeframe) - # that job generates a OpenProject::Events::AGGREGATED_..._JOURNAL_READY event. - Journals::CompletedJob.schedule(payload[:journal], payload[:send_notification]) -end + # A job is scheduled for the end of the journal aggregation time. If the journal does still exist + # at the end (it might be replaced because another journal was created within that timeframe) + # that job generates a OpenProject::Events::AGGREGATED_..._JOURNAL_READY event. + Journals::CompletedJob.schedule(payload[:journal], payload[:send_notification]) + end -OpenProject::Notifications.subscribe(OpenProject::Events::JOURNAL_AGGREGATE_BEFORE_DESTROY) do |payload| - Notifications::AggregatedJournalService.relocate_immediate(**payload.slice(:journal, :predecessor)) -end + OpenProject::Notifications.subscribe(OpenProject::Events::JOURNAL_AGGREGATE_BEFORE_DESTROY) do |payload| + Notifications::AggregatedJournalService.relocate_immediate(**payload.slice(:journal, :predecessor)) + end -OpenProject::Notifications.subscribe(OpenProject::Events::WATCHER_ADDED) do |payload| - next unless payload[:send_notifications] + OpenProject::Notifications.subscribe(OpenProject::Events::WATCHER_ADDED) do |payload| + next unless payload[:send_notifications] - Mails::WatcherAddedJob - .perform_later(payload[:watcher], - payload[:watcher_setter]) -end + Mails::WatcherAddedJob + .perform_later(payload[:watcher], + payload[:watcher_setter]) + end -OpenProject::Notifications.subscribe(OpenProject::Events::WATCHER_REMOVED) do |payload| - Mails::WatcherRemovedJob - .perform_later(payload[:watcher].attributes, - payload[:watcher_remover]) -end + OpenProject::Notifications.subscribe(OpenProject::Events::WATCHER_REMOVED) do |payload| + Mails::WatcherRemovedJob + .perform_later(payload[:watcher].attributes, + payload[:watcher_remover]) + end -OpenProject::Notifications.subscribe(OpenProject::Events::MEMBER_CREATED) do |payload| - next unless payload[:send_notifications] + OpenProject::Notifications.subscribe(OpenProject::Events::MEMBER_CREATED) do |payload| + next unless payload[:send_notifications] - Mails::MemberCreatedJob - .perform_later(current_user: User.current, - member: payload[:member], - message: payload[:message]) -end + Mails::MemberCreatedJob + .perform_later(current_user: User.current, + member: payload[:member], + message: payload[:message]) + end -OpenProject::Notifications.subscribe(OpenProject::Events::MEMBER_UPDATED) do |payload| - next unless payload[:send_notifications] + OpenProject::Notifications.subscribe(OpenProject::Events::MEMBER_UPDATED) do |payload| + next unless payload[:send_notifications] - Mails::MemberUpdatedJob - .perform_later(current_user: User.current, - member: payload[:member], - message: payload[:message]) -end + Mails::MemberUpdatedJob + .perform_later(current_user: User.current, + member: payload[:member], + message: payload[:message]) + end -OpenProject::Notifications.subscribe(OpenProject::Events::NEWS_COMMENT_CREATED) do |payload| - Notifications::WorkflowJob - .perform_later(:create_notifications, - payload[:comment], - payload[:send_notification]) + OpenProject::Notifications.subscribe(OpenProject::Events::NEWS_COMMENT_CREATED) do |payload| + Notifications::WorkflowJob + .perform_later(:create_notifications, + payload[:comment], + payload[:send_notification]) + end end diff --git a/lib/open_project/notifications.rb b/lib_static/open_project/notifications.rb similarity index 100% rename from lib/open_project/notifications.rb rename to lib_static/open_project/notifications.rb diff --git a/modules/github_integration/lib/open_project/github_integration/engine.rb b/modules/github_integration/lib/open_project/github_integration/engine.rb index 3bfc7466f3..a0a3eb8e67 100644 --- a/modules/github_integration/lib/open_project/github_integration/engine.rb +++ b/modules/github_integration/lib/open_project/github_integration/engine.rb @@ -53,13 +53,15 @@ module OpenProject::GithubIntegration end end - initializer 'github.subscribe_to_notifications' do - ::OpenProject::Notifications.subscribe('github.check_run', - &NotificationHandler.method(:check_run)) - ::OpenProject::Notifications.subscribe('github.issue_comment', - &NotificationHandler.method(:issue_comment)) - ::OpenProject::Notifications.subscribe('github.pull_request', - &NotificationHandler.method(:pull_request)) + initializer 'github.subscribe_to_notifications' do |app| + app.config.after_initialize do + ::OpenProject::Notifications.subscribe('github.check_run', + &NotificationHandler.method(:check_run)) + ::OpenProject::Notifications.subscribe('github.issue_comment', + &NotificationHandler.method(:issue_comment)) + ::OpenProject::Notifications.subscribe('github.pull_request', + &NotificationHandler.method(:pull_request)) + end end extend_api_response(:v3, :work_packages, :work_package, diff --git a/modules/webhooks/lib/open_project/webhooks/engine.rb b/modules/webhooks/lib/open_project/webhooks/engine.rb index 5cce8e5b45..5bb43ea2aa 100644 --- a/modules/webhooks/lib/open_project/webhooks/engine.rb +++ b/modules/webhooks/lib/open_project/webhooks/engine.rb @@ -45,8 +45,10 @@ module OpenProject::Webhooks caption: :'webhooks.plural' end - initializer 'webhooks.subscribe_to_notifications' do - ::OpenProject::Webhooks::EventResources.subscribe! + initializer 'webhooks.subscribe_to_notifications' do |app| + app.config.after_initialize do + ::OpenProject::Webhooks::EventResources.subscribe! + end end end end From 49471bbd661f5b86c7daf7f7366f67a70791722b Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 4 Apr 2022 17:45:45 +0200 Subject: [PATCH 10/80] static op/authentication --- config/application.rb | 16 ++----- ...-load_lib_static.rb => 00-load_plugins.rb} | 8 ---- config/initializers/warden.rb | 44 +++++++------------ .../open_project/authentication.rb | 26 +++++++++++ .../authentication/failure_app.rb | 0 .../open_project/authentication/manager.rb | 0 .../authentication/session_expiry.rb | 0 .../strategies/warden/anonymous_fallback.rb | 0 .../strategies/warden/basic_auth_failure.rb | 0 .../strategies/warden/doorkeeper_oauth.rb | 0 .../strategies/warden/global_basic_auth.rb | 0 .../strategies/warden/session.rb | 0 .../strategies/warden/user_basic_auth.rb | 0 modules/bim/lib/open_project/bim/engine.rb | 18 ++++---- 14 files changed, 56 insertions(+), 56 deletions(-) rename config/initializers/{00-load_lib_static.rb => 00-load_plugins.rb} (83%) rename {lib => lib_static}/open_project/authentication.rb (89%) rename {lib => lib_static}/open_project/authentication/failure_app.rb (100%) rename {lib => lib_static}/open_project/authentication/manager.rb (100%) rename {lib => lib_static}/open_project/authentication/session_expiry.rb (100%) rename {lib => lib_static}/open_project/authentication/strategies/warden/anonymous_fallback.rb (100%) rename {lib => lib_static}/open_project/authentication/strategies/warden/basic_auth_failure.rb (100%) rename {lib => lib_static}/open_project/authentication/strategies/warden/doorkeeper_oauth.rb (100%) rename {lib => lib_static}/open_project/authentication/strategies/warden/global_basic_auth.rb (100%) rename {lib => lib_static}/open_project/authentication/strategies/warden/session.rb (100%) rename {lib => lib_static}/open_project/authentication/strategies/warden/user_basic_auth.rb (100%) diff --git a/config/application.rb b/config/application.rb index b88fea55c3..22a16c8c27 100644 --- a/config/application.rb +++ b/config/application.rb @@ -39,18 +39,6 @@ ActiveSupport::Deprecation.silenced = (Rails.env.test? && ENV['CI']) if defined?(Bundler) - # lib directory has to be added to the load path so that - # the open_project/plugins files can be found (places under lib). - # Now it would be possible to remove that and use require with - # lib included but some plugins already use - # - # require 'open_project/plugins' - # - # to ensure the code to be loaded. So we provide a compatibility - # layer here. One might remove this later. - $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib' - require 'open_project/plugins' - # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups(:opf_plugins)) @@ -103,6 +91,10 @@ module OpenProject config.paths.add Rails.root.join('lib').to_s, eager_load: true config.paths.add Rails.root.join('lib/constraints').to_s, eager_load: true + # Constants in lib_static should only be loaded once and never be unloaded. + # That directory contains configurations and patches to rails core functionality. + config.autoload_once_paths << Rails.root.join('lib_static').to_s + # Use our own error rendering for prettier error pages config.exceptions_app = routes diff --git a/config/initializers/00-load_lib_static.rb b/config/initializers/00-load_plugins.rb similarity index 83% rename from config/initializers/00-load_lib_static.rb rename to config/initializers/00-load_plugins.rb index ebf63d5458..cce972bb14 100644 --- a/config/initializers/00-load_lib_static.rb +++ b/config/initializers/00-load_plugins.rb @@ -28,14 +28,6 @@ # TODO: check if this can be postponed and if some plugins can make use of the ActiveSupport.on_load hooks -require Rails.root.join('lib_static/redmine/i18n') -require Rails.root.join('lib_static/redmine/diff') -require Rails.root.join('lib_static/redmine/diff/diffable') -require Rails.root.join('lib_static/redmine/diff/array_string_diff') - -require Rails.root.join('lib_static/plugins/load_path_helper') -require Rails.root.join('lib_static/open_project/notifications') - # Loads the core plugins located in lib_static/plugins Dir.glob(Rails.root.join('lib_static/plugins/*')).each do |directory| if File.directory?(directory) diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb index 9623f22d68..efc05c9bf6 100644 --- a/config/initializers/warden.rb +++ b/config/initializers/warden.rb @@ -1,32 +1,20 @@ -require 'open_project/authentication' +Rails.application.config.after_initialize do + WS = OpenProject::Authentication::Strategies::Warden -# Strategies provided by OpenProject: -require 'open_project/authentication/strategies/warden/basic_auth_failure' -require 'open_project/authentication/strategies/warden/global_basic_auth' -require 'open_project/authentication/strategies/warden/user_basic_auth' -require 'open_project/authentication/strategies/warden/doorkeeper_oauth' -require 'open_project/authentication/strategies/warden/session' + strategies = [ + [:basic_auth_failure, WS::BasicAuthFailure, 'Basic'], + [:global_basic_auth, WS::GlobalBasicAuth, 'Basic'], + [:user_basic_auth, WS::UserBasicAuth, 'Basic'], + [:oauth, WS::DoorkeeperOAuth, 'OAuth'], + [:anonymous_fallback, WS::AnonymousFallback, 'Basic'], + [:session, WS::Session, 'Session'] + ] -WS = OpenProject::Authentication::Strategies::Warden + strategies.each do |name, clazz, auth_scheme| + OpenProject::Authentication.add_strategy name, clazz, auth_scheme + end -strategies = [ - [:basic_auth_failure, WS::BasicAuthFailure, 'Basic'], - [:global_basic_auth, WS::GlobalBasicAuth, 'Basic'], - [:user_basic_auth, WS::UserBasicAuth, 'Basic'], - [:oauth, WS::DoorkeeperOAuth, 'OAuth'], - [:anonymous_fallback, WS::AnonymousFallback, 'Basic'], - [:session, WS::Session, 'Session'] -] - -strategies.each do |name, clazz, auth_scheme| - OpenProject::Authentication.add_strategy name, clazz, auth_scheme -end - -include OpenProject::Authentication::Scope - -api_v3_options = { - store: false -} -OpenProject::Authentication.update_strategies(API_V3, api_v3_options) do |_strategies| - %i[global_basic_auth user_basic_auth basic_auth_failure oauth session anonymous_fallback] + OpenProject::Authentication.update_strategies(OpenProject::Authentication::Scope::API_V3, { store: false }) do |_| + %i[global_basic_auth user_basic_auth basic_auth_failure oauth session anonymous_fallback] + end end diff --git a/lib/open_project/authentication.rb b/lib_static/open_project/authentication.rb similarity index 89% rename from lib/open_project/authentication.rb rename to lib_static/open_project/authentication.rb index f1a5ba31c8..a5af59b24d 100644 --- a/lib/open_project/authentication.rb +++ b/lib_static/open_project/authentication.rb @@ -1,3 +1,29 @@ +# OpenProject is an open source project management software. +# Copyright (C) 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 'open_project/authentication/manager' module OpenProject diff --git a/lib/open_project/authentication/failure_app.rb b/lib_static/open_project/authentication/failure_app.rb similarity index 100% rename from lib/open_project/authentication/failure_app.rb rename to lib_static/open_project/authentication/failure_app.rb diff --git a/lib/open_project/authentication/manager.rb b/lib_static/open_project/authentication/manager.rb similarity index 100% rename from lib/open_project/authentication/manager.rb rename to lib_static/open_project/authentication/manager.rb diff --git a/lib/open_project/authentication/session_expiry.rb b/lib_static/open_project/authentication/session_expiry.rb similarity index 100% rename from lib/open_project/authentication/session_expiry.rb rename to lib_static/open_project/authentication/session_expiry.rb diff --git a/lib/open_project/authentication/strategies/warden/anonymous_fallback.rb b/lib_static/open_project/authentication/strategies/warden/anonymous_fallback.rb similarity index 100% rename from lib/open_project/authentication/strategies/warden/anonymous_fallback.rb rename to lib_static/open_project/authentication/strategies/warden/anonymous_fallback.rb diff --git a/lib/open_project/authentication/strategies/warden/basic_auth_failure.rb b/lib_static/open_project/authentication/strategies/warden/basic_auth_failure.rb similarity index 100% rename from lib/open_project/authentication/strategies/warden/basic_auth_failure.rb rename to lib_static/open_project/authentication/strategies/warden/basic_auth_failure.rb diff --git a/lib/open_project/authentication/strategies/warden/doorkeeper_oauth.rb b/lib_static/open_project/authentication/strategies/warden/doorkeeper_oauth.rb similarity index 100% rename from lib/open_project/authentication/strategies/warden/doorkeeper_oauth.rb rename to lib_static/open_project/authentication/strategies/warden/doorkeeper_oauth.rb diff --git a/lib/open_project/authentication/strategies/warden/global_basic_auth.rb b/lib_static/open_project/authentication/strategies/warden/global_basic_auth.rb similarity index 100% rename from lib/open_project/authentication/strategies/warden/global_basic_auth.rb rename to lib_static/open_project/authentication/strategies/warden/global_basic_auth.rb diff --git a/lib/open_project/authentication/strategies/warden/session.rb b/lib_static/open_project/authentication/strategies/warden/session.rb similarity index 100% rename from lib/open_project/authentication/strategies/warden/session.rb rename to lib_static/open_project/authentication/strategies/warden/session.rb diff --git a/lib/open_project/authentication/strategies/warden/user_basic_auth.rb b/lib_static/open_project/authentication/strategies/warden/user_basic_auth.rb similarity index 100% rename from lib/open_project/authentication/strategies/warden/user_basic_auth.rb rename to lib_static/open_project/authentication/strategies/warden/user_basic_auth.rb diff --git a/modules/bim/lib/open_project/bim/engine.rb b/modules/bim/lib/open_project/bim/engine.rb index b6944aa87a..712c497630 100644 --- a/modules/bim/lib/open_project/bim/engine.rb +++ b/modules/bim/lib/open_project/bim/engine.rb @@ -199,16 +199,18 @@ module OpenProject::Bim Mime::Type.register "application/octet-stream", :bcfzip unless Mime::Type.lookup_by_extension(:bcfzip) end - initializer 'bim.bcf.add_api_scope' do - Doorkeeper.configuration.scopes.add(:bcf_v2_1) + initializer 'bim.bcf.add_api_scope' do |app| + app.config.before_initialize do + Doorkeeper.configuration.scopes.add(:bcf_v2_1) - module OpenProject::Authentication::Scope - BCF_V2_1 = :bcf_v2_1 - end + module OpenProject::Authentication::Scope + BCF_V2_1 = :bcf_v2_1 + end - OpenProject::Authentication.update_strategies(OpenProject::Authentication::Scope::BCF_V2_1, - store: false) do |_strategies| - %i[oauth session] + OpenProject::Authentication.update_strategies(OpenProject::Authentication::Scope::BCF_V2_1, + store: false) do |_strategies| + %i[oauth session] + end end end From 13bae7fa571adbd5ac9fddf30b957c3fb357df3b Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 4 Apr 2022 17:52:53 +0200 Subject: [PATCH 11/80] turn op/database static --- {lib => lib_static}/open_project/database.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {lib => lib_static}/open_project/database.rb (100%) diff --git a/lib/open_project/database.rb b/lib_static/open_project/database.rb similarity index 100% rename from lib/open_project/database.rb rename to lib_static/open_project/database.rb From 02d4133195740db62b4b83a8e7da76193b580a07 Mon Sep 17 00:00:00 2001 From: Dombi Attila Date: Mon, 9 May 2022 20:27:47 +0300 Subject: [PATCH 12/80] Make work packages table sorting case insensitive --- .rubocop.yml | 3 +- app/models/query/results.rb | 71 +++- app/models/query/results/group_by.rb | 36 +- spec/models/query/results_spec.rb | 491 ++++++++++++++++++++------- 4 files changed, 449 insertions(+), 152 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 6abcb51bc2..86c10efd0b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -172,8 +172,7 @@ RSpec/MultipleExpectations: - 'modules/*/spec/features/**/*.rb' RSpec/MultipleMemoizedHelpers: - Max: 20 - AllowSubject: true + Enabled: false RSpec/NestedGroups: Max: 4 diff --git a/app/models/query/results.rb b/app/models/query/results.rb index 33d2db44fd..b8aea4ed8d 100644 --- a/app/models/query/results.rb +++ b/app/models/query/results.rb @@ -123,25 +123,74 @@ class ::Query::Results criteria = ::Query::SortCriteria.new query.sortable_columns criteria.available_criteria = aliased_sorting_by_column_name criteria.criteria = query.sort_criteria - criteria.map_each { |criteria| criteria.map { |raw| Arel.sql raw } } + criteria.map_each { |c| c.map { |raw| Arel.sql raw } } end def aliased_sorting_by_column_name sorting_by_column_name = query.sortable_key_by_column_name - aliases = include_aliases + reflections = reflection_includes + + sorting_by_column_name.each_with_object({}) do |(column_key, sortable), hash| + column_is_association = reflections.include?(column_key.to_sym) + columns_hash = columns_hash_for(column_is_association ? column_key : nil) + hash[column_key] = if column_is_association + alias_name = aliases[column_key.to_sym] + expand_association_columns(alias_name, sortable, columns_hash) + else + case_insensitive_condition(column_key, sortable, columns_hash) + end + end + end - reflection_includes.each do |inc| - sorting_by_column_name[inc.to_s] = Array(sorting_by_column_name[inc.to_s]).map do |column| - if column.respond_to?(:call) - column.call(aliases[inc]) - else - "#{aliases[inc]}.#{column}" - end - end + ## + # Returns the expanded association columns name + def expand_association_columns(alias_name, sortable, columns_hash) + Array(sortable).map do |column| + sort_condition = expand_association_column(column, alias_name) + case_insensitive_condition(column, sort_condition, columns_hash) end + end - sorting_by_column_name + ## + # Returns a single expanded association column name + def expand_association_column(column, alias_name) + if column.respond_to?(:call) + column.call(alias_name) + else + "#{alias_name}.#{column}" + end + end + + ## + # Return the columns hash for a given association + # If the association is nil, then return the WorkPackage.columns_hash + def columns_hash_for(association = nil) + if association + WorkPackage.reflections[association].klass.columns_hash + else + WorkPackage.columns_hash + end + end + + ## + # Return the case insensitive version for columns with a string type + def case_insensitive_condition(column_key, condition, columns_hash) + if columns_hash[column_key]&.type == :string + "LOWER(#{condition})" + elsif custom_field_type(column_key) == "string" + condition.map { |c| "LOWER(#{c})" } + else + condition + end + end + + ## + # Find the custom field type based on the column key + def custom_field_type(column_key) + (column = query.sortable_columns.detect { |c| c.name.to_s == column_key }) && + column.respond_to?(:custom_field) && + column.custom_field.field_format end # To avoid naming conflicts, joined tables are aliased if they are joined diff --git a/app/models/query/results/group_by.rb b/app/models/query/results/group_by.rb index 068aca0e9f..841970739d 100644 --- a/app/models/query/results/group_by.rb +++ b/app/models/query/results/group_by.rb @@ -29,13 +29,11 @@ module ::Query::Results::GroupBy # Returns the work package count by group or nil if query is not grouped def work_package_count_by_group - @work_package_count_by_group ||= begin - if query.grouped? - r = group_counts_by_group + @work_package_count_by_group ||= if query.grouped? + r = group_counts_by_group - transform_group_keys(r) - end - end + transform_group_keys(r) + end rescue ::ActiveRecord::StatementInvalid => e raise ::Query::StatementInvalid.new(e.message) end @@ -65,7 +63,7 @@ module ::Query::Results::GroupBy def group_by_for_count Array(query.group_by_statement).map { |statement| Arel.sql(statement) } + - [Arel.sql(group_by_sort(false))] + [Arel.sql(group_by_sort(order: false))] end def pluck_for_count @@ -137,33 +135,33 @@ module ::Query::Results::GroupBy def transform_association_property_keys(association, groups) ar_keys = association.class_name.constantize.find(groups.keys.compact) - groups.map do |key, value| - [ar_keys.detect { |ar_key| ar_key.id == key }, value] - end.to_h + groups.transform_keys do |key| + ar_keys.detect { |ar_key| ar_key.id == key } + end end # Returns the SQL sort order that should be prepended for grouping - def group_by_sort(order = true) + def group_by_sort(order: true) if query.grouped? && (column = query.group_by_column) - aliases = include_aliases - + alias_name = include_aliases[column.name] + columns_hash = columns_hash_for(alias_name ? column.association : nil) Array(column.sortable).map do |s| direction = order ? order_for_group_by(column) : nil - aliased_group_by_sort_order(aliases[column.name], s, direction) + aliased_group_by_sort_order(alias_name, s, columns_hash, direction) end.join(', ') end end - def aliased_group_by_sort_order(alias_name, sortable, order = nil) - column = if alias_name && sortable.respond_to?(:call) - sortable.call(alias_name) - elsif alias_name - "#{alias_name}.#{sortable}" + def aliased_group_by_sort_order(alias_name, sortable, columns_hash, order = nil) + column = if alias_name + expand_association_column(sortable, alias_name) else sortable end + column = case_insensitive_condition(sortable, column, columns_hash) + if order column + " #{order} " else diff --git a/spec/models/query/results_spec.rb b/spec/models/query/results_spec.rb index 8da8901b0e..1f1019380c 100644 --- a/spec/models/query/results_spec.rb +++ b/spec/models/query/results_spec.rb @@ -34,9 +34,9 @@ describe ::Query::Results, type: :model, with_mail: false do show_hierarchies: false end let(:query_results) do - ::Query::Results.new query + described_class.new query end - let(:project_1) { create :project } + let(:project1) { create :project } let(:role_pm) do create(:role, permissions: %i( @@ -50,18 +50,18 @@ describe ::Query::Results, type: :model, with_mail: false do create(:role, permissions: [:view_work_packages]) end - let(:user_1) do + let(:user1) do create(:user, firstname: 'user', lastname: '1', - member_in_project: project_1, + member_in_project: project1, member_through_role: [role_dev, role_pm]) end let(:wp_p1) do (1..3).map do create(:work_package, - project: project_1, - assigned_to_id: user_1.id) + project: project1, + assigned_to_id: user1.id) end end @@ -70,67 +70,67 @@ describe ::Query::Results, type: :model, with_mail: false do build :query, show_hierarchies: false, group_by: group_by, - project: project_1 + project: project1 end - let(:type_1) do + let(:type1) do create(:type) end - let(:type_2) do + let(:type2) do create(:type) end let(:work_package1) do create(:work_package, - type: type_1, - project: project_1) + type: type1, + project: project1) end let(:work_package2) do create(:work_package, - type: type_2, - project: project_1) + type: type2, + project: project1) end - context 'grouping by responsible' do + context 'when grouping by responsible' do let(:group_by) { 'responsible' } - it 'should produce a valid SQL statement' do + it 'produces a valid SQL statement' do expect { query_results.work_package_count_by_group }.not_to raise_error end end - context 'grouping and filtering by text' do + context 'when grouping and filtering by text' do let(:group_by) { 'responsible' } before do query.add_filter('search', '**', ['asdf']) end - it 'should produce a valid SQL statement (Regression #29598)' do + it 'produces a valid SQL statement (Regression #29598)' do expect { query_results.work_package_count_by_group }.not_to raise_error end end - context 'grouping by assigned_to' do + context 'when grouping by assigned_to' do let(:group_by) { 'assigned_to' } before do work_package1 - work_package2.update_column(:assigned_to_id, user_1.id) + work_package2.update_column(:assigned_to_id, user1.id) - login_as(user_1) + login_as(user1) end it 'returns a hash of counts by value' do - expect(query_results.work_package_count_by_group).to eql(nil => 1, user_1 => 1) + expect(query_results.work_package_count_by_group).to eql(nil => 1, user1 => 1) end end - context 'grouping by assigned_to with only a nil group' do + context 'when grouping by assigned_to with only a nil group' do let(:group_by) { 'assigned_to' } before do work_package1 - login_as(user_1) + login_as(user1) end it 'returns a hash of counts by value' do @@ -138,44 +138,44 @@ describe ::Query::Results, type: :model, with_mail: false do end end - context 'grouping by type' do + context 'when grouping by type' do let(:group_by) { 'type' } before do work_package1 work_package2 - login_as(user_1) + login_as(user1) end it 'returns the groups sorted by type`s position' do - type_1.update_column(:position, 1) - type_2.update_column(:position, 2) + type1.update_column(:position, 1) + type2.update_column(:position, 2) result = query_results.work_package_count_by_group expect(result.length) - .to eql 2 + .to be 2 expect(result.keys.map(&:id)) - .to eql [type_1.id, type_2.id] + .to eql [type1.id, type2.id] - type_1.update_column(:position, 2) - type_2.update_column(:position, 1) + type1.update_column(:position, 2) + type2.update_column(:position, 1) - new_results = ::Query::Results.new(query) + new_results = described_class.new(query) result = new_results.work_package_count_by_group expect(result.length) - .to eql 2 + .to be 2 expect(result.keys.map(&:id)) - .to eql [type_2.id, type_1.id] + .to eql [type2.id, type1.id] end end - context 'grouping by list custom field and filtering for it at the same time' do + context 'when grouping by list custom field and filtering for it at the same time' do let!(:custom_field) do create(:list_wp_custom_field, is_for_all: true, @@ -194,7 +194,7 @@ describe ::Query::Results, type: :model, with_mail: false do let(:group_by) { "cf_#{custom_field.id}" } before do - login_as(user_1) + login_as(user1) work_package1.send(:"custom_field_#{custom_field.id}=", first_value) work_package1.save! @@ -210,13 +210,13 @@ describe ::Query::Results, type: :model, with_mail: false do expected_groups = [[first_value], [first_value, last_value]] group_count.each do |key, count| - expect(count).to eql 1 - expect(expected_groups.any? { |group| group & key == key & group }).to be_truthy + expect(count).to be 1 + expect(expected_groups).to(be_any { |group| group & key == key & group }) end end end - context 'grouping by int custom field' do + context 'when grouping by int custom field' do let!(:custom_field) do create(:int_wp_custom_field, is_for_all: true, is_filter: true) end @@ -224,10 +224,10 @@ describe ::Query::Results, type: :model, with_mail: false do let(:group_by) { "cf_#{custom_field.id}" } before do - login_as(user_1) + login_as(user1) wp_p1[0].type.custom_fields << custom_field - project_1.work_package_custom_fields << custom_field + project1.work_package_custom_fields << custom_field wp_p1[0].update_attribute(:"custom_field_#{custom_field.id}", 42) wp_p1[0].save @@ -240,7 +240,7 @@ describe ::Query::Results, type: :model, with_mail: false do end end - context 'grouping by user custom field' do + context 'when grouping by user custom field' do let!(:custom_field) do create(:user_wp_custom_field, is_for_all: true, is_filter: true) end @@ -248,10 +248,10 @@ describe ::Query::Results, type: :model, with_mail: false do let(:group_by) { "cf_#{custom_field.id}" } before do - login_as(user_1) + login_as(user1) wp_p1[0].type.custom_fields << custom_field - project_1.work_package_custom_fields << custom_field + project1.work_package_custom_fields << custom_field end it 'returns nil as user custom fields are not groupable' do @@ -259,7 +259,7 @@ describe ::Query::Results, type: :model, with_mail: false do end end - context 'grouping by bool custom field' do + context 'when grouping by bool custom field' do let!(:custom_field) do create(:bool_wp_custom_field, is_for_all: true, is_filter: true) end @@ -267,10 +267,10 @@ describe ::Query::Results, type: :model, with_mail: false do let(:group_by) { "cf_#{custom_field.id}" } before do - login_as(user_1) + login_as(user1) wp_p1[0].type.custom_fields << custom_field - project_1.work_package_custom_fields << custom_field + project1.work_package_custom_fields << custom_field wp_p1[0].update_attribute(:"custom_field_#{custom_field.id}", true) wp_p1[0].save @@ -283,7 +283,7 @@ describe ::Query::Results, type: :model, with_mail: false do end end - context 'grouping by date custom field' do + context 'when grouping by date custom field' do let!(:custom_field) do create(:date_wp_custom_field, is_for_all: true, is_filter: true) end @@ -291,49 +291,49 @@ describe ::Query::Results, type: :model, with_mail: false do let(:group_by) { "cf_#{custom_field.id}" } before do - login_as(user_1) + login_as(user1) wp_p1[0].type.custom_fields << custom_field - project_1.work_package_custom_fields << custom_field + project1.work_package_custom_fields << custom_field - wp_p1[0].update_attribute(:"custom_field_#{custom_field.id}", Date.today) + wp_p1[0].update_attribute(:"custom_field_#{custom_field.id}", Time.zone.today) wp_p1[0].save - wp_p1[1].update_attribute(:"custom_field_#{custom_field.id}", Date.today) + wp_p1[1].update_attribute(:"custom_field_#{custom_field.id}", Time.zone.today) wp_p1[1].save end it 'returns a hash of counts by value' do - expect(query_results.work_package_count_by_group).to eql(Date.today => 2, nil => 1) + expect(query_results.work_package_count_by_group).to eql(Time.zone.today => 2, nil => 1) end end end - describe '#work_packages' do - let!(:project_1) { create :project } - let!(:project_2) { create :project } + describe 'filtering' do + let!(:project1) { create :project } + let!(:project2) { create :project } let!(:member) do create(:member, - project: project_2, - principal: user_1, + project: project2, + principal: user1, roles: [role_pm]) end - let!(:user_2) do + let!(:user2) do create(:user, firstname: 'user', lastname: '2', - member_in_project: project_2, + member_in_project: project2, member_through_role: role_dev) end let!(:wp_p2) do create(:work_package, - project: project_2, - assigned_to_id: user_2.id) + project: project2, + assigned_to_id: user2.id) end let!(:wp2_p2) do create(:work_package, - project: project_2, - assigned_to_id: user_1.id) + project: project2, + assigned_to_id: user1.id) end before do @@ -342,16 +342,16 @@ describe ::Query::Results, type: :model, with_mail: false do context 'when filtering for assigned_to_role' do before do - allow(User).to receive(:current).and_return(user_2) - allow(project_2.descendants).to receive(:active).and_return([]) + allow(User).to receive(:current).and_return(user2) + allow(project2.descendants).to receive(:active).and_return([]) query.add_filter('assigned_to_role', '=', [role_dev.id.to_s]) end context 'when a project is set' do - let(:query) { build :query, project: project_2 } + let(:query) { build :query, project: project2 } - it 'should display only wp for selected project and selected role' do + it 'displays only wp for selected project and selected role' do expect(query_results.work_packages).to match_array([wp_p2]) end end @@ -359,7 +359,7 @@ describe ::Query::Results, type: :model, with_mail: false do context 'when no project is set' do let(:query) { build :query, project: nil } - it 'should display all wp from projects where User.current has access' do + it 'displays all wp from projects where User.current has access' do expect(query_results.work_packages).to match_array([wp_p2, wp2_p2]) end end @@ -373,13 +373,13 @@ describe ::Query::Results, type: :model, with_mail: false do build_stubbed :query, show_hierarchies: false, group_by: group_by, - project: project_2 + project: project2 end let!(:custom_field) { create(:work_package_custom_field, is_for_all: true) } before do - allow(User).to receive(:current).and_return(user_2) + allow(User).to receive(:current).and_return(user2) # reload in order to have the custom field as an available # custom field @@ -427,31 +427,31 @@ describe ::Query::Results, type: :model, with_mail: false do build :query, show_hierarchies: false, group_by: group_by, - project: project_1 + project: project1 end let(:group_by) { 'responsible' } before do - allow(User).to receive(:current).and_return(user_1) + allow(User).to receive(:current).and_return(user1) - wp_p1[0].update_attribute(:responsible, user_1) - wp_p1[1].update_attribute(:responsible, user_2) + wp_p1[0].update_attribute(:responsible, user1) + wp_p1[1].update_attribute(:responsible, user2) end it 'outputs the work package count in the schema { => count }' do expect(query_results.work_package_count_by_group) - .to eql(user_1 => 1, user_2 => 1, nil => 1) + .to eql(user1 => 1, user2 => 1, nil => 1) end end context 'when filtering by precedes and ordering by id' do let(:query) do build :query, - project: project_1 + project: project1 end before do - login_as(user_1) + login_as(user1) wp_p1[1].precedes << wp_p1[0] @@ -467,10 +467,10 @@ describe ::Query::Results, type: :model, with_mail: false do end end - describe '#work_packages' do - let(:work_package1) { create(:work_package, project: project_1, id: 1) } - let(:work_package2) { create(:work_package, project: project_1, id: 2) } - let(:work_package3) { create(:work_package, project: project_1, id: 3) } + describe 'sorting' do + let(:work_package1) { create(:work_package, project: project1, id: 1) } + let(:work_package2) { create(:work_package, project: project1, id: 2) } + let(:work_package3) { create(:work_package, project: project1, id: 3) } let(:sort_by) { [['id', 'asc']] } let(:columns) { %i(id subject) } let(:group_by) { '' } @@ -480,24 +480,24 @@ describe ::Query::Results, type: :model, with_mail: false do show_hierarchies: false, group_by: group_by, sort_criteria: sort_by, - project: project_1, + project: project1, column_names: columns end let(:query_results) do - ::Query::Results.new query + described_class.new query end let(:user_a) { create(:user, firstname: 'AAA', lastname: 'AAA') } - let(:user_m) { create(:user, firstname: 'MMM', lastname: 'MMM') } + let(:user_m) { create(:user, firstname: 'mmm', lastname: 'mmm') } let(:user_z) { create(:user, firstname: 'ZZZ', lastname: 'ZZZ') } - context 'grouping by assigned_to, having the author column selected' do + context 'when grouping by assigned_to, having the author column selected' do let(:group_by) { 'assigned_to' } let(:columns) { %i(id subject author) } before do - allow(User).to receive(:current).and_return(user_1) + allow(User).to receive(:current).and_return(user1) work_package1.assigned_to = user_m work_package1.author = user_m @@ -515,7 +515,7 @@ describe ::Query::Results, type: :model, with_mail: false do work_package3.save(validate: false) end - it 'sorts first by assigned_to (group by), then by sort criteria' do + it 'sorts case insensitive first by assigned_to (group by), then by sort criteria' do # Would look like this in the table # # user_m @@ -528,12 +528,12 @@ describe ::Query::Results, type: :model, with_mail: false do end end - context 'sorting by author, grouping by assigned_to' do + context 'when sorting by author, grouping by assigned_to' do let(:group_by) { 'assigned_to' } let(:sort_by) { [['author', 'asc']] } before do - allow(User).to receive(:current).and_return(user_1) + allow(User).to receive(:current).and_return(user1) work_package1.assigned_to = user_m work_package1.author = user_m @@ -551,7 +551,7 @@ describe ::Query::Results, type: :model, with_mail: false do work_package3.save(validate: false) end - it 'sorts first by group by, then by assigned_to' do + it 'sorts case insensitive first by group by, then by assigned_to' do # Would look like this in the table # # user_m @@ -576,13 +576,13 @@ describe ::Query::Results, type: :model, with_mail: false do end end - context 'sorting and grouping by priority' do + context 'when sorting and grouping by priority' do let(:prio_low) { create :issue_priority, position: 1 } let(:prio_high) { create :issue_priority, position: 0 } let(:group_by) { 'priority' } before do - allow(User).to receive(:current).and_return(user_1) + allow(User).to receive(:current).and_return(user1) work_package1.priority = prio_low work_package2.priority = prio_high @@ -604,13 +604,13 @@ describe ::Query::Results, type: :model, with_mail: false do end end - context 'sorting by priority, grouping by project' do + context 'when sorting by priority, grouping by project' do let(:prio_low) { create :issue_priority, position: 1 } let(:prio_high) { create :issue_priority, position: 0 } let(:group_by) { 'project' } before do - allow(User).to receive(:current).and_return(user_1) + allow(User).to receive(:current).and_return(user1) work_package1.priority = prio_low work_package2.priority = prio_high @@ -632,16 +632,16 @@ describe ::Query::Results, type: :model, with_mail: false do group_count = query_results.work_package_count_by_group - expect(group_count).to eq({ project_1 => 2 }) + expect(group_count).to eq({ project1 => 2 }) end end - context 'sorting by author and responsible, grouping by assigned_to' do + context 'when sorting by author and responsible, grouping by assigned_to' do let(:group_by) { 'assigned_to' } let(:sort_by) { [['author', 'asc'], ['responsible', 'desc']] } before do - allow(User).to receive(:current).and_return(user_1) + allow(User).to receive(:current).and_return(user1) work_package1.assigned_to = user_m work_package1.author = user_m @@ -662,7 +662,7 @@ describe ::Query::Results, type: :model, with_mail: false do work_package3.save(validate: false) end - it 'sorts first by group by, then by assigned_to (neutral as equal), then by responsible' do + it 'sorts case insensitive first by group by, then by assigned_to (neutral as equal), then by responsible' do # Would look like this in the table # # user_m @@ -687,7 +687,274 @@ describe ::Query::Results, type: :model, with_mail: false do end end - context 'filtering by bool cf' do + context 'when sorting by project' do + let(:user1) { create(:admin) } + let(:query) do + build_stubbed :query, + show_hierarchies: false, + project: nil, + sort_criteria: sort_by + end + + let(:project1) { create :project, name: 'Project A' } + let(:project2) { create :project, name: 'Project b' } + let(:project3) { create :project, name: 'Project C' } + let(:work_package1) { create(:work_package, project: project1) } + let(:work_package2) { create(:work_package, project: project2) } + let(:work_package3) { create(:work_package, project: project3) } + + before { login_as(user1) } + + context 'when ascending' do + let(:sort_by) { [['project', 'asc']] } + + it 'sorts case insensitive' do + expect(query_results.work_packages) + .to match [work_package1, work_package2, work_package3] + end + end + + context 'when descending' do + let(:sort_by) { [['project', 'desc']] } + + it 'sorts case insensitive' do + expect(query_results.work_packages) + .to match [work_package3, work_package2, work_package1] + end + end + end + + context 'when sorting by category' do + let(:user1) { create(:admin) } + let(:query) do + build_stubbed :query, + show_hierarchies: false, + project: nil, + sort_criteria: sort_by + end + let(:category1) { create(:category, project: project1, name: 'Category A') } + let(:category2) { create(:category, project: project1, name: 'Category b') } + let(:category3) { create(:category, project: project1, name: 'Category C') } + let(:work_package1) { create(:work_package, project: project1, category: category1) } + let(:work_package2) { create(:work_package, project: project1, category: category2) } + let(:work_package3) { create(:work_package, project: project1, category: category3) } + + before { login_as(user1) } + + context 'when ascending' do + let(:sort_by) { [['category', 'asc']] } + + it 'sorts case insensitive' do + query_results.work_packages + [work_package1, work_package2, work_package3] + + expect(query_results.work_packages) + .to match [work_package1, work_package2, work_package3] + end + end + + context 'when descending' do + let(:sort_by) { [['category', 'desc']] } + + it 'sorts case insensitive' do + expect(query_results.work_packages) + .to match [work_package3, work_package2, work_package1] + end + end + end + + context 'when sorting by subject' do + let(:user1) { create(:admin) } + let(:query) do + build_stubbed :query, + show_hierarchies: false, + project: nil, + sort_criteria: sort_by + end + let(:work_package1) { create(:work_package, project: project1, subject: 'WorkPackage A') } + let(:work_package2) { create(:work_package, project: project1, subject: 'WorkPackage b') } + let(:work_package3) { create(:work_package, project: project1, subject: 'WorkPackage C') } + + before { login_as(user1) } + + context 'when ascending' do + let(:sort_by) { [['subject', 'asc']] } + + it 'sorts case insensitive' do + query_results.work_packages + [work_package1, work_package2, work_package3] + + expect(query_results.work_packages) + .to match [work_package1, work_package2, work_package3] + end + end + + context 'when descending' do + let(:sort_by) { [['subject', 'desc']] } + + it 'sorts case insensitive' do + expect(query_results.work_packages) + .to match [work_package3, work_package2, work_package1] + end + end + end + + context 'when sorting by finish date' do + let(:user1) { create(:admin) } + let(:query) do + build_stubbed :query, + show_hierarchies: false, + project: nil, + sort_criteria: sort_by + end + let(:work_package1) { create(:work_package, project: project1, due_date: 3.days.ago) } + let(:work_package2) { create(:work_package, project: project1, due_date: 2.days.ago) } + let(:work_package3) { create(:work_package, project: project1, due_date: 1.day.ago) } + + before { login_as(user1) } + + context 'when ascending' do + let(:sort_by) { [['due_date', 'asc']] } + + it 'sorts case insensitive' do + expect(query_results.work_packages) + .to match [work_package1, work_package2, work_package3] + end + end + + context 'when descending' do + let(:sort_by) { [['due_date', 'desc']] } + + it 'sorts case insensitive' do + expect(query_results.work_packages) + .to match [work_package3, work_package2, work_package1] + end + end + end + + context 'when sorting by string custom field' do + let(:user1) { create(:admin) } + let(:query) do + build_stubbed :query, + show_hierarchies: false, + project: nil, + sort_criteria: sort_by + end + + let(:work_package1) { create(:work_package, project: project1) } + let(:work_package2) { create(:work_package, project: project1) } + let(:work_package3) { create(:work_package, project: project1) } + let(:string_cf) { create(:string_wp_custom_field, is_filter: true) } + let!(:custom_value) do + create(:custom_value, + custom_field: string_cf, + customized: work_package1, + value: 'String A') + end + let!(:custom_value2) do + create(:custom_value, + custom_field: string_cf, + customized: work_package2, + value: 'String b') + end + + let!(:custom_value3) do + create(:custom_value, + custom_field: string_cf, + customized: work_package3, + value: 'String C') + end + + before do + work_package1.project.work_package_custom_fields << string_cf + work_package1.type.custom_fields << string_cf + + work_package1.reload + project1.reload + login_as(user1) + end + + context 'when ascending' do + let(:sort_by) { [["cf_#{string_cf.id}", 'asc']] } + + it 'sorts case insensitive' do + expect(query_results.work_packages) + .to match [work_package1, work_package2, work_package3] + end + end + + context 'when descending' do + let(:sort_by) { [["assigned_to", 'desc']] } + + it 'sorts case insensitive' do + expect(query_results.work_packages) + .to match [work_package3, work_package2, work_package1] + end + end + end + + context 'when sorting by integer custom field' do + let(:user1) { create(:admin) } + let(:query) do + build_stubbed :query, + show_hierarchies: false, + project: nil, + sort_criteria: sort_by + end + + let(:work_package1) { create(:work_package, project: project1) } + let(:work_package2) { create(:work_package, project: project1) } + let(:work_package3) { create(:work_package, project: project1) } + let(:int_cf) { create(:int_wp_custom_field, is_filter: true) } + let!(:custom_value) do + create(:custom_value, + custom_field: int_cf, + customized: work_package1, + value: 1) + end + let!(:custom_value2) do + create(:custom_value, + custom_field: int_cf, + customized: work_package2, + value: 2) + end + + let!(:custom_value3) do + create(:custom_value, + custom_field: int_cf, + customized: work_package3, + value: 3) + end + + before do + work_package1.project.work_package_custom_fields << int_cf + work_package1.type.custom_fields << int_cf + + work_package1.reload + project1.reload + login_as(user1) + end + + context 'when ascending' do + let(:sort_by) { [["cf_#{int_cf.id}", 'asc']] } + + it 'sorts case insensitive' do + expect(query_results.work_packages) + .to match [work_package1, work_package2, work_package3] + end + end + + context 'when descending' do + let(:sort_by) { [["cf_#{int_cf.id}", 'desc']] } + + it 'sorts case insensitive' do + expect(query_results.work_packages) + .to match [work_package3, work_package2, work_package1] + end + end + end + + context 'when filtering by bool cf' do let(:bool_cf) { create(:bool_wp_custom_field, is_filter: true) } let(:custom_value) do create(:custom_value, @@ -702,11 +969,11 @@ describe ::Query::Results, type: :model, with_mail: false do work_package1.type.custom_fields << bool_cf work_package1.reload - project_1.reload + project1.reload end before do - allow(User).to receive(:current).and_return(user_1) + allow(User).to receive(:current).and_return(user1) custom_value @@ -771,22 +1038,6 @@ describe ::Query::Results, type: :model, with_mail: false do it_behaves_like 'returns the wp' end - context 'with the wp having no value for the cf - and filtering for false - and the cf not being active in the project' do - let(:custom_value) { nil } - let(:filter_value) { 'f' } - - let(:activate_cf) do - work_package1.type.custom_fields << bool_cf - - work_package1.reload - project_1.reload - end - - it_behaves_like 'is empty' - end - context 'with the wp having no value for the cf and filtering for false and the cf not being active for the type' do @@ -797,7 +1048,7 @@ describe ::Query::Results, type: :model, with_mail: false do work_package1.type.custom_fields << bool_cf work_package1.reload - project_1.reload + project1.reload end it_behaves_like 'is empty' @@ -819,7 +1070,7 @@ describe ::Query::Results, type: :model, with_mail: false do work_package1.project.work_package_custom_fields << bool_cf work_package1.reload - project_1.reload + project1.reload end it_behaves_like 'is empty' From e670a84d64b6889edd8a54cc36bdab63829d3114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 17 May 2022 16:12:56 +0200 Subject: [PATCH 13/80] Put status job listener in prepare block --- .../job_status/lib/open_project/job_status/engine.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/modules/job_status/lib/open_project/job_status/engine.rb b/modules/job_status/lib/open_project/job_status/engine.rb index 7d09fb4a72..b89297da6b 100644 --- a/modules/job_status/lib/open_project/job_status/engine.rb +++ b/modules/job_status/lib/open_project/job_status/engine.rb @@ -46,16 +46,14 @@ module OpenProject::JobStatus "#{root}/job_statuses/#{uuid}" end - initializer 'job_status.event_listener' do + config.to_prepare do + # Register the cron job to clear statuses periodically + ::Cron::CronJob.register! ::JobStatus::Cron::ClearOldJobStatusJob + # Extends the ActiveJob adapter in use (DelayedJob) by a Status which lives # indenpendently from the job itself (which is deleted once successful or after max attempts). # That way, the result of a background job is available even after the original job is gone. EventListener.register! end - - config.to_prepare do - # Register the cron job to clear statuses periodically - ::Cron::CronJob.register! ::JobStatus::Cron::ClearOldJobStatusJob - end end end From ea0356b4efcf5d7ac0101d7c058f70b3c67c4e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 17 May 2022 16:13:02 +0200 Subject: [PATCH 14/80] Load patches in autoloader --- config/initializers/10-load_patches.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/config/initializers/10-load_patches.rb b/config/initializers/10-load_patches.rb index 5caab4bf3a..9c99e96c3c 100644 --- a/config/initializers/10-load_patches.rb +++ b/config/initializers/10-load_patches.rb @@ -26,10 +26,12 @@ # See COPYRIGHT and LICENSE files for more details. #++ -# Do not place any patches within this file. Add a file to lib/open_project/patches -require 'open_project/patches' +Rails.application.reloader.to_prepare do + # Do not place any patches within this file. Add a file to lib/open_project/patches + require 'open_project/patches' -# Whatever ruby file is placed in lib/open_project/patches is required -Dir.glob(File.expand_path('../../lib/open_project/patches/*.rb', __dir__)).each do |path| - require path + # Whatever ruby file is placed in lib/open_project/patches is required + Dir.glob(File.expand_path('../../lib/open_project/patches/*.rb', __dir__)).each do |path| + require path + end end From 6578ea442393d1e59ef1c8ecb961ee21b33c6f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 17 May 2022 16:21:47 +0200 Subject: [PATCH 15/80] Move grape initializer into reloader --- config/initializers/grape.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/config/initializers/grape.rb b/config/initializers/grape.rb index 3c86ab4a63..d3b68e29b2 100644 --- a/config/initializers/grape.rb +++ b/config/initializers/grape.rb @@ -25,9 +25,10 @@ # # See COPYRIGHT and LICENSE files for more details. #++ - -module Grape - class Endpoint - include ::API::V3::Utilities::PathHelper +Rails.application.reloader.to_prepare do + module Grape + class Endpoint + include ::API::V3::Utilities::PathHelper + end end end From ca32144ffa5709ed74b5730afa33a6bb5b87db15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 17 May 2022 16:21:54 +0200 Subject: [PATCH 16/80] Move bim engine registration in prepare --- modules/bim/lib/open_project/bim/engine.rb | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/modules/bim/lib/open_project/bim/engine.rb b/modules/bim/lib/open_project/bim/engine.rb index 712c497630..878a9f2780 100644 --- a/modules/bim/lib/open_project/bim/engine.rb +++ b/modules/bim/lib/open_project/bim/engine.rb @@ -199,18 +199,16 @@ module OpenProject::Bim Mime::Type.register "application/octet-stream", :bcfzip unless Mime::Type.lookup_by_extension(:bcfzip) end - initializer 'bim.bcf.add_api_scope' do |app| - app.config.before_initialize do - Doorkeeper.configuration.scopes.add(:bcf_v2_1) + config.to_prepare do + Doorkeeper.configuration.scopes.add(:bcf_v2_1) - module OpenProject::Authentication::Scope - BCF_V2_1 = :bcf_v2_1 - end + module OpenProject::Authentication::Scope + BCF_V2_1 = :bcf_v2_1 + end - OpenProject::Authentication.update_strategies(OpenProject::Authentication::Scope::BCF_V2_1, - store: false) do |_strategies| - %i[oauth session] - end + OpenProject::Authentication.update_strategies(OpenProject::Authentication::Scope::BCF_V2_1, + store: false) do |_strategies| + %i[oauth session] end end From e5358ea5aff1b020a2632defbff6c4e059b6d65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 17 May 2022 16:21:59 +0200 Subject: [PATCH 17/80] Move rack_timeout in reloader --- config/initializers/rack_timeout.rb | 36 +++++++++++++++-------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/config/initializers/rack_timeout.rb b/config/initializers/rack_timeout.rb index 27e48f0744..947c035b2f 100644 --- a/config/initializers/rack_timeout.rb +++ b/config/initializers/rack_timeout.rb @@ -14,30 +14,32 @@ if OpenProject::Configuration.web_workers >= 2 term_on_timeout: 1 # shut down worker (gracefully) right away on timeout to be restarted ) - # remove default logger (logging uninteresting extra info with each not timed out request) - Rack::Timeout.unregister_state_change_observer(:logger) + Rails.application.reloader.to_prepare do + # remove default logger (logging uninteresting extra info with each not timed out request) + Rack::Timeout.unregister_state_change_observer(:logger) - Rack::Timeout.register_state_change_observer(:wait_timeout_logger) do |env| - details = env[Rack::Timeout::ENV_INFO_KEY] + Rack::Timeout.register_state_change_observer(:wait_timeout_logger) do |env| + details = env[Rack::Timeout::ENV_INFO_KEY] - if details.state == :timed_out && details.wait.present? - ::OpenProject.logger.error "Request timed out waiting to be served!" + if details.state == :timed_out && details.wait.present? + ::OpenProject.logger.error "Request timed out waiting to be served!" + end end - end - # The timeout itself is already reported so no need to - # report the generic internal server error too as it doesn't - # add any more information. Even worse, it's not immediately - # clear that the two reports are related. - module SuppressInternalErrorReportOnTimeout - def op_handle_error(message_or_exception, context = {}) - return if request && request.env[Rack::Timeout::ENV_INFO_KEY].try(:state) == :timed_out + # The timeout itself is already reported so no need to + # report the generic internal server error too as it doesn't + # add any more information. Even worse, it's not immediately + # clear that the two reports are related. + module SuppressInternalErrorReportOnTimeout + def op_handle_error(message_or_exception, context = {}) + return if request && request.env[Rack::Timeout::ENV_INFO_KEY].try(:state) == :timed_out - super + super + end end - end - OpenProjectErrorHelper.prepend SuppressInternalErrorReportOnTimeout + OpenProjectErrorHelper.prepend SuppressInternalErrorReportOnTimeout + end else Rails.logger.debug { "Not enabling Rack::Timeout since we are not running in cluster mode with at least 2 workers" } end From 8ec3f05767590b4eba0b5ace8cc27a932111f472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 17 May 2022 16:22:57 +0200 Subject: [PATCH 18/80] Move interceptors registry into reloader --- config/initializers/register_mail_interceptors.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/config/initializers/register_mail_interceptors.rb b/config/initializers/register_mail_interceptors.rb index b6c9329bb3..3137ca16b9 100644 --- a/config/initializers/register_mail_interceptors.rb +++ b/config/initializers/register_mail_interceptors.rb @@ -28,7 +28,8 @@ # Register interceptors defined in app/mailers/user_mailer.rb # Do this here, so they aren't registered multiple times due to reloading in development mode. - -ApplicationMailer.register_interceptor Interceptors::DefaultHeaders -# following needs to be the last interceptor -ApplicationMailer.register_interceptor Interceptors::DoNotSendMailsWithoutRecipient +Rails.application.reloader.to_prepare do + ApplicationMailer.register_interceptor Interceptors::DefaultHeaders + # following needs to be the last interceptor + ApplicationMailer.register_interceptor Interceptors::DoNotSendMailsWithoutRecipient +end From 9f96e2926e389ad3d13a029f98c658586db52e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 17 May 2022 16:25:12 +0200 Subject: [PATCH 19/80] Move secure_headers into reloader --- config/initializers/secure_headers.rb | 126 +++++++++++++------------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/config/initializers/secure_headers.rb b/config/initializers/secure_headers.rb index df59aaed40..c3caf8765d 100644 --- a/config/initializers/secure_headers.rb +++ b/config/initializers/secure_headers.rb @@ -1,75 +1,77 @@ -SecureHeaders::Configuration.default do |config| - config.cookies = { - secure: true, - httponly: true - } - # Add "; preload" and submit the site to hstspreload.org for best protection. - config.hsts = "max-age=#{20.years.to_i}; includeSubdomains" - config.x_frame_options = "SAMEORIGIN" - config.x_content_type_options = "nosniff" - config.x_xss_protection = "1; mode=block" - config.x_permitted_cross_domain_policies = "none" - config.referrer_policy = "origin-when-cross-origin" +Rails.application.reloader.to_prepare do + SecureHeaders::Configuration.default do |config| + config.cookies = { + secure: true, + httponly: true + } + # Add "; preload" and submit the site to hstspreload.org for best protection. + config.hsts = "max-age=#{20.years.to_i}; includeSubdomains" + config.x_frame_options = "SAMEORIGIN" + config.x_content_type_options = "nosniff" + config.x_xss_protection = "1; mode=block" + config.x_permitted_cross_domain_policies = "none" + config.referrer_policy = "origin-when-cross-origin" - # Valid for assets - assets_src = ["'self'"] - asset_host = OpenProject::Configuration.rails_asset_host - assets_src << asset_host if asset_host.present? + # Valid for assets + assets_src = ["'self'"] + asset_host = OpenProject::Configuration.rails_asset_host + assets_src << asset_host if asset_host.present? - # Valid for iframes - frame_src = %w['self' https://player.vimeo.com] - frame_src << OpenProject::Configuration[:security_badge_url] + # Valid for iframes + frame_src = %w['self' https://player.vimeo.com] + frame_src << OpenProject::Configuration[:security_badge_url] - # Default src - default_src = %w('self') + OpenProject::Configuration.remote_storage_hosts + # Default src + default_src = %w('self') + OpenProject::Configuration.remote_storage_hosts - # Allow requests to CLI in dev mode - connect_src = default_src + [OpenProject::Configuration.enterprise_trial_creation_host] + # Allow requests to CLI in dev mode + connect_src = default_src + [OpenProject::Configuration.enterprise_trial_creation_host] - if OpenProject::Configuration.sentry_frontend_dsn.present? - connect_src += [OpenProject::Configuration.sentry_host] - end + if OpenProject::Configuration.sentry_frontend_dsn.present? + connect_src += [OpenProject::Configuration.sentry_host] + end - # Add proxy configuration for Angular CLI to csp - if FrontendAssetHelper.assets_proxied? - proxied = ['ws://localhost:*', 'http://localhost:*', FrontendAssetHelper.cli_proxy] - connect_src += proxied - assets_src += proxied - end + # Add proxy configuration for Angular CLI to csp + if FrontendAssetHelper.assets_proxied? + proxied = ['ws://localhost:*', 'http://localhost:*', FrontendAssetHelper.cli_proxy] + connect_src += proxied + assets_src += proxied + end - # Allow to extend the script-src in specific situations - script_src = assets_src + # Allow to extend the script-src in specific situations + script_src = assets_src - # Allow unsafe-eval for rack-mini-profiler - if Rails.env.development? && ENV['OPENPROJECT_RACK_PROFILER_ENABLED'] - script_src += %w('unsafe-eval') - end + # Allow unsafe-eval for rack-mini-profiler + if Rails.env.development? && ENV['OPENPROJECT_RACK_PROFILER_ENABLED'] + script_src += %w('unsafe-eval') + end - config.csp = { - preserve_schemes: true, + config.csp = { + preserve_schemes: true, - # Fallback when no value is defined - default_src: default_src, - # Allowed uri in tag - base_uri: %w('self'), + # Fallback when no value is defined + default_src: default_src, + # Allowed uri in tag + base_uri: %w('self'), - # Allow fonts from self, asset host, or DATA uri - font_src: assets_src + %w(data:), - # Form targets can only be self - form_action: default_src, - # Allow iframe from vimeo (welcome video) - frame_src: frame_src + %w('self'), - frame_ancestors: %w('self'), - # Allow images from anywhere including data urls and blobs (used in resizing) - img_src: %w(* data: blob:), - # Allow scripts from self - script_src: script_src, - # Allow unsafe-inline styles - style_src: assets_src + %w('unsafe-inline'), - # Allow object-src from Release API - object_src: [OpenProject::Configuration[:security_badge_url]], + # Allow fonts from self, asset host, or DATA uri + font_src: assets_src + %w(data:), + # Form targets can only be self + form_action: default_src, + # Allow iframe from vimeo (welcome video) + frame_src: frame_src + %w('self'), + frame_ancestors: %w('self'), + # Allow images from anywhere including data urls and blobs (used in resizing) + img_src: %w(* data: blob:), + # Allow scripts from self + script_src: script_src, + # Allow unsafe-inline styles + style_src: assets_src + %w('unsafe-inline'), + # Allow object-src from Release API + object_src: [OpenProject::Configuration[:security_badge_url]], - # Connect sources for CLI in dev mode - connect_src: connect_src - } + # Connect sources for CLI in dev mode + connect_src: connect_src + } + end end From 67946ce2866eb00feb0e27d8c86ce83aea99984d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 17 May 2022 16:25:20 +0200 Subject: [PATCH 20/80] Move sentry into reloader --- config/initializers/sentry.rb | 144 +++++++++++++++++----------------- 1 file changed, 73 insertions(+), 71 deletions(-) diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index f4d0d3f8dc..0c18a359dc 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -1,87 +1,89 @@ if OpenProject::Logging::SentryLogger.enabled? - Sentry.init do |config| - config.dsn = OpenProject::Logging::SentryLogger.sentry_dsn - config.breadcrumbs_logger = OpenProject::Configuration.sentry_breadcrumb_loggers.map(&:to_sym) - - # Output debug info for sentry - config.before_send = lambda do |event, hint| - Rails.logger.debug do - payload_sizes = event.to_json_compatible.transform_values { |v| JSON.generate(v).bytesize }.inspect - "[Sentry] will send event #{hint}. Payload sizes are #{payload_sizes.inspect}" + Rails.application.reloader.to_prepare do + Sentry.init do |config| + config.dsn = OpenProject::Logging::SentryLogger.sentry_dsn + config.breadcrumbs_logger = OpenProject::Configuration.sentry_breadcrumb_loggers.map(&:to_sym) + + # Output debug info for sentry + config.before_send = lambda do |event, hint| + Rails.logger.debug do + payload_sizes = event.to_json_compatible.transform_values { |v| JSON.generate(v).bytesize }.inspect + "[Sentry] will send event #{hint}. Payload sizes are #{payload_sizes.inspect}" + end + + event end - event - end - - # Add plugins with openproject/open_project in backtraces to internal - config.app_dirs_pattern = /(bin|exe|app|config|lib|open_?project)/ + # Add plugins with openproject/open_project in backtraces to internal + config.app_dirs_pattern = /(bin|exe|app|config|lib|open_?project)/ - # Don't send loaded modules - config.send_modules = false + # Don't send loaded modules + config.send_modules = false - # Disable sentry's internal client reports - config.send_client_reports = false + # Disable sentry's internal client reports + config.send_client_reports = false - # Cleanup backtrace - config.backtrace_cleanup_callback = lambda do |backtrace| - Rails.backtrace_cleaner.clean(backtrace) - end + # Cleanup backtrace + config.backtrace_cleanup_callback = lambda do |backtrace| + Rails.backtrace_cleaner.clean(backtrace) + end - # Sample rate for performance - # 0.0 = disabled - # 1.0 = all samples are traced - sample_factor = OpenProject::Configuration.sentry_trace_factor.to_f - - # Define a tracing sample handler - trace_sampler = lambda do |sampling_context| - # if this is the continuation of a trace, just use that decision (rate controlled by the caller) - next sampling_context[:parent_sampled] unless sampling_context[:parent_sampled].nil? - - # transaction_context is the transaction object in hash form - # keep in mind that sampling happens right after the transaction is initialized - # for example, at the beginning of the request - transaction_context = sampling_context[:transaction_context] - - # transaction_context helps you sample transactions with more sophistication - # for example, you can provide different sample rates based on the operation or name - op = transaction_context[:op] - transaction_name = transaction_context[:name] - - rate = case op - when /request/ - case transaction_name - when /health_check/ - 0.0 + # Sample rate for performance + # 0.0 = disabled + # 1.0 = all samples are traced + sample_factor = OpenProject::Configuration.sentry_trace_factor.to_f + + # Define a tracing sample handler + trace_sampler = lambda do |sampling_context| + # if this is the continuation of a trace, just use that decision (rate controlled by the caller) + next sampling_context[:parent_sampled] unless sampling_context[:parent_sampled].nil? + + # transaction_context is the transaction object in hash form + # keep in mind that sampling happens right after the transaction is initialized + # for example, at the beginning of the request + transaction_context = sampling_context[:transaction_context] + + # transaction_context helps you sample transactions with more sophistication + # for example, you can provide different sample rates based on the operation or name + op = transaction_context[:op] + transaction_name = transaction_context[:name] + + rate = case op + when /request/ + case transaction_name + when /health_check/ + 0.0 + else + [0.1 * sample_factor, 1.0].min + end + when /delayed_job/ + [0.01 * sample_factor, 1.0].min else - [0.1 * sample_factor, 1.0].min + 0.0 # ignore all other transactions end - when /delayed_job/ - [0.01 * sample_factor, 1.0].min - else - 0.0 # ignore all other transactions - end - Rails.logger.debug { "[SENTRY TRACE SAMPLER] Decided on sampling rate #{rate} for #{op}: #{transaction_name} " } + Rails.logger.debug { "[SENTRY TRACE SAMPLER] Decided on sampling rate #{rate} for #{op}: #{transaction_name} " } - rate - end + rate + end + + # Assign the sampler conditionally to avoid running the lambda + # when we don't trace anyway + if sample_factor.zero? + Rails.logger.debug { "[SENTRY TRACE SAMPLER] Requested factor is zero, skipping performance tracing" } + config.traces_sample_rate = 0 + config.traces_sampler = nil + else + Rails.logger.debug { "[SENTRY TRACE SAMPLER] Requested factor is #{sample_factor}, setting up performance tracing" } + config.traces_sampler = trace_sampler + end - # Assign the sampler conditionally to avoid running the lambda - # when we don't trace anyway - if sample_factor.zero? - Rails.logger.debug { "[SENTRY TRACE SAMPLER] Requested factor is zero, skipping performance tracing" } - config.traces_sample_rate = 0 - config.traces_sampler = nil - else - Rails.logger.debug { "[SENTRY TRACE SAMPLER] Requested factor is #{sample_factor}, setting up performance tracing" } - config.traces_sampler = trace_sampler + # Set release info + config.release = OpenProject::VERSION.to_s end - # Set release info - config.release = OpenProject::VERSION.to_s + # Extend the core log delegator + handler = ::OpenProject::Logging::SentryLogger.method(:log) + ::OpenProject::Logging::LogDelegator.register(:sentry, handler) end - - # Extend the core log delegator - handler = ::OpenProject::Logging::SentryLogger.method(:log) - ::OpenProject::Logging::LogDelegator.register(:sentry, handler) end From 6e5b92c45728c7cff5910f20daef87e76ce05c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 17 May 2022 16:27:10 +0200 Subject: [PATCH 21/80] Linting --- config/initializers/grape.rb | 2 ++ config/initializers/rack_timeout.rb | 2 ++ config/initializers/secure_headers.rb | 4 +++- config/initializers/warden.rb | 14 +++++++------- modules/bim/lib/open_project/bim/engine.rb | 4 +++- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/config/initializers/grape.rb b/config/initializers/grape.rb index d3b68e29b2..bad5b6e72d 100644 --- a/config/initializers/grape.rb +++ b/config/initializers/grape.rb @@ -26,9 +26,11 @@ # See COPYRIGHT and LICENSE files for more details. #++ Rails.application.reloader.to_prepare do + # rubocop:disable Lint/ConstantDefinitionInBlock module Grape class Endpoint include ::API::V3::Utilities::PathHelper end end + # rubocop:enable Lint/ConstantDefinitionInBlock end diff --git a/config/initializers/rack_timeout.rb b/config/initializers/rack_timeout.rb index 947c035b2f..9d93c6a8b7 100644 --- a/config/initializers/rack_timeout.rb +++ b/config/initializers/rack_timeout.rb @@ -30,6 +30,7 @@ if OpenProject::Configuration.web_workers >= 2 # report the generic internal server error too as it doesn't # add any more information. Even worse, it's not immediately # clear that the two reports are related. + # rubocop:disable Lint/ConstantDefinitionInBlock module SuppressInternalErrorReportOnTimeout def op_handle_error(message_or_exception, context = {}) return if request && request.env[Rack::Timeout::ENV_INFO_KEY].try(:state) == :timed_out @@ -40,6 +41,7 @@ if OpenProject::Configuration.web_workers >= 2 OpenProjectErrorHelper.prepend SuppressInternalErrorReportOnTimeout end + # rubocop:enable Lint/ConstantDefinitionInBlock else Rails.logger.debug { "Not enabling Rack::Timeout since we are not running in cluster mode with at least 2 workers" } end diff --git a/config/initializers/secure_headers.rb b/config/initializers/secure_headers.rb index c3caf8765d..99b259db0a 100644 --- a/config/initializers/secure_headers.rb +++ b/config/initializers/secure_headers.rb @@ -1,3 +1,4 @@ +# rubocop:disable Lint/PercentStringArray Rails.application.reloader.to_prepare do SecureHeaders::Configuration.default do |config| config.cookies = { @@ -42,7 +43,7 @@ Rails.application.reloader.to_prepare do script_src = assets_src # Allow unsafe-eval for rack-mini-profiler - if Rails.env.development? && ENV['OPENPROJECT_RACK_PROFILER_ENABLED'] + if Rails.env.development? && ENV.fetch('OPENPROJECT_RACK_PROFILER_ENABLED', false) script_src += %w('unsafe-eval') end @@ -75,3 +76,4 @@ Rails.application.reloader.to_prepare do } end end +# rubocop:enable Lint/PercentStringArray diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb index efc05c9bf6..53762fad9c 100644 --- a/config/initializers/warden.rb +++ b/config/initializers/warden.rb @@ -1,13 +1,13 @@ Rails.application.config.after_initialize do - WS = OpenProject::Authentication::Strategies::Warden + namespace = OpenProject::Authentication::Strategies::Warden strategies = [ - [:basic_auth_failure, WS::BasicAuthFailure, 'Basic'], - [:global_basic_auth, WS::GlobalBasicAuth, 'Basic'], - [:user_basic_auth, WS::UserBasicAuth, 'Basic'], - [:oauth, WS::DoorkeeperOAuth, 'OAuth'], - [:anonymous_fallback, WS::AnonymousFallback, 'Basic'], - [:session, WS::Session, 'Session'] + [:basic_auth_failure, namespace::BasicAuthFailure, 'Basic'], + [:global_basic_auth, namespace::GlobalBasicAuth, 'Basic'], + [:user_basic_auth, namespace::UserBasicAuth, 'Basic'], + [:oauth, namespace::DoorkeeperOAuth, 'OAuth'], + [:anonymous_fallback, namespace::AnonymousFallback, 'Basic'], + [:session, namespace::Session, 'Session'] ] strategies.each do |name, clazz, auth_scheme| diff --git a/modules/bim/lib/open_project/bim/engine.rb b/modules/bim/lib/open_project/bim/engine.rb index 878a9f2780..ff9cfb1c2e 100644 --- a/modules/bim/lib/open_project/bim/engine.rb +++ b/modules/bim/lib/open_project/bim/engine.rb @@ -199,11 +199,12 @@ module OpenProject::Bim Mime::Type.register "application/octet-stream", :bcfzip unless Mime::Type.lookup_by_extension(:bcfzip) end + # rubocop:disable Naming/VariableNumber config.to_prepare do Doorkeeper.configuration.scopes.add(:bcf_v2_1) module OpenProject::Authentication::Scope - BCF_V2_1 = :bcf_v2_1 + BCF_V2_1 = :bcf_v2_1 # rubocop:disable Lint/ConstantDefinitionInBlock end OpenProject::Authentication.update_strategies(OpenProject::Authentication::Scope::BCF_V2_1, @@ -211,6 +212,7 @@ module OpenProject::Bim %i[oauth session] end end + # rubocop:enable Naming/VariableNumber config.to_prepare do ::Exports::Register.register do From 843bc458970a70e88f333e337d35538de7733970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 17 May 2022 22:11:30 +0200 Subject: [PATCH 22/80] Clear caches when mapping new modules in access control --- lib/open_project/access_control.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/open_project/access_control.rb b/lib/open_project/access_control.rb index bbc8f1db5e..96ddbd3bc7 100644 --- a/lib/open_project/access_control.rb +++ b/lib/open_project/access_control.rb @@ -40,6 +40,8 @@ module OpenProject @modules += mapper.mapped_modules @project_modules_without_permissions ||= [] @project_modules_without_permissions += mapper.project_modules_without_permissions + + clear_caches end # Get a sorted array of module names From 558c4b314ba2c18ef6dd401f2efd48476e0a77aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 17 May 2022 22:11:51 +0200 Subject: [PATCH 23/80] Fix unsetting the @project_scope instance variable on reload, this variable obviously is no longer present --- lib/redmine/plugin.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/redmine/plugin.rb b/lib/redmine/plugin.rb index a62b8c0cc5..19906009d4 100644 --- a/lib/redmine/plugin.rb +++ b/lib/redmine/plugin.rb @@ -331,13 +331,13 @@ module Redmine #:nodoc: # permission :destroy_contacts, { contacts: :destroy } # end def project_module(name, options = {}, &block) - @project_scope = [name, options] plugin = self Rails.application.reloader.to_prepare do + plugin.instance_eval { @project_scope = [name, options] } plugin.instance_eval(&block) end ensure - @project_scope = nil + plugin.instance_eval { @project_scope = nil } end # Registers an activity provider. From 553b0b4b19435971b98b0cfd0454932ef530cb2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 18 May 2022 11:37:42 +0200 Subject: [PATCH 24/80] Move configuration to static lib --- {lib => lib_static}/open_project/configuration.rb | 0 {lib => lib_static}/open_project/configuration/asset_host.rb | 0 {lib => lib_static}/open_project/configuration/helpers.rb | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {lib => lib_static}/open_project/configuration.rb (100%) rename {lib => lib_static}/open_project/configuration/asset_host.rb (100%) rename {lib => lib_static}/open_project/configuration/helpers.rb (100%) diff --git a/lib/open_project/configuration.rb b/lib_static/open_project/configuration.rb similarity index 100% rename from lib/open_project/configuration.rb rename to lib_static/open_project/configuration.rb diff --git a/lib/open_project/configuration/asset_host.rb b/lib_static/open_project/configuration/asset_host.rb similarity index 100% rename from lib/open_project/configuration/asset_host.rb rename to lib_static/open_project/configuration/asset_host.rb diff --git a/lib/open_project/configuration/helpers.rb b/lib_static/open_project/configuration/helpers.rb similarity index 100% rename from lib/open_project/configuration/helpers.rb rename to lib_static/open_project/configuration/helpers.rb From 22b7849520dc9684b8049d3f5ec1b423c3566c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 18 May 2022 13:50:00 +0200 Subject: [PATCH 25/80] Remove legacy mailer configuration and move into settings Split from "Remove Setting.protocol in favor of static config" Restore cache store setup --- app/controllers/application_controller.rb | 6 +- app/models/setting.rb | 1 + app/models/setting/mail_settings.rb | 78 ++++++ app/workers/application_job.rb | 6 +- config/application.rb | 4 +- config/initializers/cache_store.rb | 32 +++ config/initializers/migrate_email_settings.rb | 10 - lib_static/open_project/configuration.rb | 123 +--------- spec/lib/open_project/configuration_spec.rb | 230 ------------------ spec/mailers/smtp_settings_spec.rb | 4 +- spec/models/setting_spec.rb | 117 +++++++++ 11 files changed, 243 insertions(+), 368 deletions(-) create mode 100644 app/models/setting/mail_settings.rb create mode 100644 config/initializers/cache_store.rb delete mode 100644 config/initializers/migrate_email_settings.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5606ab455e..355194396f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -139,7 +139,7 @@ class ApplicationController < ActionController::Base :stop_if_feeds_disabled, :set_cache_buster, :action_hooks, - :reload_mailer_configuration! + :reload_mailer_settings! include Redmine::Search::Controller include Redmine::MenuManager::MenuController @@ -167,8 +167,8 @@ class ApplicationController < ActionController::Base end end - def reload_mailer_configuration! - OpenProject::Configuration.reload_mailer_configuration! + def reload_mailer_settings! + Setting.reload_mailer_settings! end # Checks if the session cookie is missing. diff --git a/app/models/setting.rb b/app/models/setting.rb index 14dd8b9718..764d891bff 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -29,6 +29,7 @@ class Setting < ApplicationRecord extend CallbacksHelper extend Aliases + extend MailSettings ENCODINGS = %w(US-ASCII windows-1250 diff --git a/app/models/setting/mail_settings.rb b/app/models/setting/mail_settings.rb new file mode 100644 index 0000000000..5d84b7a3c0 --- /dev/null +++ b/app/models/setting/mail_settings.rb @@ -0,0 +1,78 @@ +#-- 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. +#++ + +class Setting + module MailSettings + ## + # Reload the currently configured mailer configuration + def reload_mailer_settings! + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.delivery_method = Setting.email_delivery_method if Setting.email_delivery_method + + if Setting.email_delivery_method == :smtp + reload_smtp_settings! + end + rescue StandardError => e + Rails.logger.error "Unable to set ActionMailer settings (#{e.message}). " \ + "Email sending will most likely NOT work." + end + + private + + def reload_smtp_settings! + # Correct smtp settings when using authentication :none + authentication = Setting.smtp_authentication.try(:to_sym) + keys = %i[address port domain authentication user_name password] + if authentication == :none + # Rails Mailer will croak if passing :none as the authentication. + # Instead, it requires to be removed from its settings + ActionMailer::Base.smtp_settings.delete :user_name + ActionMailer::Base.smtp_settings.delete :password + ActionMailer::Base.smtp_settings.delete :authentication + + keys = %i[address port domain] + end + + keys.each do |setting| + value = Setting["smtp_#{setting}"] + if value.present? + ActionMailer::Base.smtp_settings[setting] = value + else + ActionMailer::Base.smtp_settings.delete setting + end + end + + ActionMailer::Base.smtp_settings[:enable_starttls_auto] = Setting.smtp_enable_starttls_auto? + ActionMailer::Base.smtp_settings[:ssl] = Setting.smtp_ssl? + + Setting.smtp_openssl_verify_mode.tap do |mode| + ActionMailer::Base.smtp_settings[:openssl_verify_mode] = mode unless mode.nil? + end + end + end +end diff --git a/app/workers/application_job.rb b/app/workers/application_job.rb index da7eca59c9..3721af444f 100644 --- a/app/workers/application_job.rb +++ b/app/workers/application_job.rb @@ -88,15 +88,15 @@ class ApplicationJob < ::ActiveJob::Base # Since the email configuration is now done in the web app, we need to # make sure that any changes to the configuration is correctly picked up # by the background jobs at runtime. - def reload_mailer_configuration! - OpenProject::Configuration.reload_mailer_configuration! + def reload_mailer_settings! + Setting.reload_mailer_settings! end private def clean_context with_clean_request_store do - reload_mailer_configuration! + reload_mailer_settings! yield end diff --git a/config/application.rb b/config/application.rb index 22a16c8c27..f9d2d84283 100644 --- a/config/application.rb +++ b/config/application.rb @@ -44,7 +44,7 @@ if defined?(Bundler) Bundler.require(*Rails.groups(:opf_plugins)) end -require_relative '../lib/open_project/configuration' +require_relative '../lib_static/open_project/configuration' module OpenProject class Application < Rails::Application @@ -177,8 +177,6 @@ module OpenProject # This allows for setting the root either via config file or via environment variable. config.action_controller.relative_url_root = OpenProject::Configuration['rails_relative_url_root'] - OpenProject::Configuration.configure_cache(config) - config.active_job.queue_adapter = :delayed_job config.action_controller.asset_host = OpenProject::Configuration::AssetHost.value diff --git a/config/initializers/cache_store.rb b/config/initializers/cache_store.rb new file mode 100644 index 0000000000..c57316fcee --- /dev/null +++ b/config/initializers/cache_store.rb @@ -0,0 +1,32 @@ +#-- 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. +#++ + +# Be sure to restart your server when you modify this file. +OpenProject::Application.configure do + OpenProject::Configuration.configure_cache(config) +end diff --git a/config/initializers/migrate_email_settings.rb b/config/initializers/migrate_email_settings.rb deleted file mode 100644 index f3b4f697cc..0000000000 --- a/config/initializers/migrate_email_settings.rb +++ /dev/null @@ -1,10 +0,0 @@ -OpenProject::Application.configure do - config.after_initialize do - # settings table may not exist when you run db:migrate at installation - # time, so just ignore this block when that happens. - if Setting.settings_table_exists_yet? - OpenProject::Configuration.migrate_mailer_configuration! - OpenProject::Configuration.reload_mailer_configuration! - end - end -end diff --git a/lib_static/open_project/configuration.rb b/lib_static/open_project/configuration.rb index 21d638de92..686a5deb68 100644 --- a/lib_static/open_project/configuration.rb +++ b/lib_static/open_project/configuration.rb @@ -45,8 +45,6 @@ module OpenProject end def configure_cache(application_config) - return unless override_cache_config? application_config - # rails defaults to :file_store, use :mem_cache_store when :memcache is configured in configuration.yml # Also use :mem_cache_store for when :dalli_store is configured cache_store = self['rails_cache_store'].try(:to_sym) @@ -55,131 +53,20 @@ module OpenProject when :memcache, :dalli_store cache_config = [:mem_cache_store] cache_config << self['cache_memcache_server'] if self['cache_memcache_server'] - # default to :file_store + # default to :file_store when NilClass, :file_store cache_config = [:file_store, Rails.root.join('tmp/cache')] else cache_config = [cache_store] end - parameters = cache_parameters - cache_config << parameters if parameters.size > 0 + parameters = cache_store_parameters + cache_config << parameters unless parameters.empty? application_config.cache_store = cache_config end - def override_cache_config?(application_config) - # override if cache store is not set - # or cache store is :file_store - # or there is something to overwrite it - application_config.cache_store.nil? || - application_config.cache_store == :file_store || - self['rails_cache_store'].present? - end - - def migrate_mailer_configuration! - # do not migrate if forced to legacy configuration (using settings or ENV) - return true if self['email_delivery_configuration'] == 'legacy' - # do not migrate if no legacy configuration - return true if self['email_delivery_method'].blank? - # do not migrate if the setting already exists and is not blank - return true if Setting.email_delivery_method.present? - - Rails.logger.info 'Migrating existing email configuration to the settings table...' - Setting.email_delivery_method = self['email_delivery_method'].to_sym - - ['smtp_', 'sendmail_'].each do |config_type| - mail_delivery_configs = Settings::Definition.all_of_prefix(config_type) - - next if mail_delivery_configs.empty? - - mail_delivery_configs.each do |config| - Setting["#{config_type}#{config.name}"] = case config.value - when TrueClass - 1 - when FalseClass - 0 - else - v - end - end - end - - true - end - - def reload_mailer_configuration! - if self['email_delivery_configuration'] == 'legacy' - configure_legacy_action_mailer - else - case Setting.email_delivery_method - when :smtp - ActionMailer::Base.perform_deliveries = true - ActionMailer::Base.delivery_method = Setting.email_delivery_method - - reload_smtp_settings! - when :sendmail - ActionMailer::Base.perform_deliveries = true - ActionMailer::Base.delivery_method = Setting.email_delivery_method - end - end - rescue StandardError => e - Rails.logger.warn "Unable to set ActionMailer settings (#{e.message}). " \ - 'Email sending will most likely NOT work.' - end - - # This is used to configure email sending from users who prefer to - # continue using environment variables of configuration.yml settings. Our - # hosted SaaS version requires this. - def configure_legacy_action_mailer - return true if self['email_delivery_method'].blank? - - ActionMailer::Base.perform_deliveries = true - ActionMailer::Base.delivery_method = self['email_delivery_method'].to_sym - - %w[smtp_ sendmail_].each do |config_type| - config = settings_of_prefix(config_type) - - next if config.empty? - - ActionMailer::Base.send("#{config_type}settings=", config) - end - end - - private - - def reload_smtp_settings! - # Correct smtp settings when using authentication :none - authentication = Setting.smtp_authentication.try(:to_sym) - keys = %i[address port domain authentication user_name password] - if authentication == :none - # Rails Mailer will croak if passing :none as the authentication. - # Instead, it requires to be removed from its settings - ActionMailer::Base.smtp_settings.delete :user_name - ActionMailer::Base.smtp_settings.delete :password - ActionMailer::Base.smtp_settings.delete :authentication - - keys = %i[address port domain] - end - - keys.each do |setting| - value = Setting["smtp_#{setting}"] - if value.present? - ActionMailer::Base.smtp_settings[setting] = value - else - ActionMailer::Base.smtp_settings.delete setting - end - end - - ActionMailer::Base.smtp_settings[:enable_starttls_auto] = Setting.smtp_enable_starttls_auto? - ActionMailer::Base.smtp_settings[:ssl] = Setting.smtp_ssl? - - Setting.smtp_openssl_verify_mode.tap do |mode| - ActionMailer::Base.smtp_settings[:openssl_verify_mode] = mode unless mode.nil? - end - end - - def cache_parameters + def cache_store_parameters mapping = { 'cache_expires_in_seconds' => %i[expires_in to_i], 'cache_namespace' => %i[namespace to_s] @@ -194,6 +81,8 @@ module OpenProject parameters end + private + def method_missing(name, *args, &block) setting_name = name.to_s.sub(/\?$/, '') diff --git a/spec/lib/open_project/configuration_spec.rb b/spec/lib/open_project/configuration_spec.rb index 653935c766..1518314d6f 100644 --- a/spec/lib/open_project/configuration_spec.rb +++ b/spec/lib/open_project/configuration_spec.rb @@ -55,225 +55,6 @@ describe OpenProject::Configuration, :settings_reset do end end - describe '.migrate_mailer_configuration!' do - before do - allow(Setting) - .to receive(:email_delivery_method=) - end - - it 'does nothing if no legacy configuration given' do - described_class['email_delivery_method'] = nil - expect(described_class.migrate_mailer_configuration!).to be_truthy - expect(Setting).not_to have_received(:email_delivery_method=) - end - - it 'does nothing if email_delivery_configuration forced to legacy' do - described_class['email_delivery_configuration'] = 'legacy' - expect(described_class.migrate_mailer_configuration!).to be_truthy - expect(Setting).not_to have_received(:email_delivery_method=) - end - - it 'does nothing if setting already set' do - described_class['email_delivery_method'] = :sendmail - allow(Setting) - .to receive(:email_delivery_method) - .and_return(:sendmail) - expect(Setting).not_to have_received(:email_delivery_method=) - expect(described_class.migrate_mailer_configuration!).to be_truthy - end - - it 'migrates the existing configuration to the settings table' do - described_class['email_delivery_method'] = :smtp - described_class['smtp_password'] = 'p4ssw0rd' - described_class['smtp_address'] = 'smtp.example.com' - described_class['smtp_port'] = 587 - described_class['smtp_user_name'] = 'username' - described_class['smtp_enable_starttls_auto'] = true - described_class['smtp_ssl'] = true - - expect(described_class.migrate_mailer_configuration!).to be_truthy - expect(Setting.email_delivery_method).to eq(:smtp) - expect(Setting.smtp_password).to eq('p4ssw0rd') - expect(Setting.smtp_address).to eq('smtp.example.com') - expect(Setting.smtp_port).to eq(587) - expect(Setting.smtp_user_name).to eq('username') - expect(Setting).to be_smtp_enable_starttls_auto - expect(Setting).to be_smtp_ssl - end - end - - describe '.reload_mailer_configuration!' do - before do - allow(ActionMailer::Base) - .to receive(:perform_deliveries=) - allow(ActionMailer::Base) - .to receive(:delivery_method=) - end - - it 'uses the legacy method to configure email settings' do - allow(described_class) - .to receive(:configure_legacy_action_mailer) - described_class['email_delivery_configuration'] = 'legacy' - described_class.reload_mailer_configuration! - expect(described_class).to have_received(:configure_legacy_action_mailer) - end - - context 'without smtp_authentication and without ssl' do - it 'uses the setting values', - with_settings: { - email_delivery_method: :smtp, - smtp_authentication: :none, - smtp_password: 'old', - smtp_address: 'smtp.example.com', - smtp_domain: 'example.com', - smtp_port: 25, - smtp_user_name: 'username', - smtp_enable_starttls_auto: 1, - smtp_ssl: 0 - } do - described_class.reload_mailer_configuration! - expect(ActionMailer::Base).to have_received(:perform_deliveries=).with(true) - expect(ActionMailer::Base).to have_received(:delivery_method=).with(:smtp) - expect(ActionMailer::Base.smtp_settings[:smtp_authentication]).to be_nil - expect(ActionMailer::Base.smtp_settings).to eq(address: 'smtp.example.com', - port: 25, - domain: 'example.com', - enable_starttls_auto: true, - openssl_verify_mode: 'peer', - ssl: false) - end - end - - context 'without smtp_authentication and with ssl' do - it 'users the setting values', - with_settings: { - email_delivery_method: :smtp, - smtp_authentication: :none, - smtp_password: 'old', - smtp_address: 'smtp.example.com', - smtp_domain: 'example.com', - smtp_port: 25, - smtp_user_name: 'username', - smtp_enable_starttls_auto: 0, - smtp_ssl: 1 - } do - described_class.reload_mailer_configuration! - expect(ActionMailer::Base).to have_received(:perform_deliveries=).with(true) - expect(ActionMailer::Base).to have_received(:delivery_method=).with(:smtp) - expect(ActionMailer::Base.smtp_settings[:smtp_authentication]).to be_nil - expect(ActionMailer::Base.smtp_settings).to eq(address: 'smtp.example.com', - port: 25, - domain: 'example.com', - enable_starttls_auto: false, - openssl_verify_mode: 'peer', - ssl: true) - end - end - - context 'with smtp_authentication and without ssl' do - it 'users the setting values', - with_settings: { - email_delivery_method: :smtp, - smtp_password: 'p4ssw0rd', - smtp_address: 'smtp.example.com', - smtp_domain: 'example.com', - smtp_port: 587, - smtp_user_name: 'username', - smtp_enable_starttls_auto: 1, - smtp_ssl: 0 - } do - described_class.reload_mailer_configuration! - expect(ActionMailer::Base).to have_received(:perform_deliveries=).with(true) - expect(ActionMailer::Base).to have_received(:delivery_method=).with(:smtp) - expect(ActionMailer::Base.smtp_settings[:smtp_authentication]).to be_nil - expect(ActionMailer::Base.smtp_settings).to eq(address: 'smtp.example.com', - port: 587, - domain: 'example.com', - authentication: 'plain', - user_name: 'username', - password: 'p4ssw0rd', - enable_starttls_auto: true, - openssl_verify_mode: 'peer', - ssl: false) - end - end - - context 'with smtp_authentication and with ssl' do - it 'users the setting values', - with_settings: { - email_delivery_method: :smtp, - smtp_password: 'p4ssw0rd', - smtp_address: 'smtp.example.com', - smtp_domain: 'example.com', - smtp_port: 587, - smtp_user_name: 'username', - smtp_enable_starttls_auto: 0, - smtp_ssl: 1 - } do - described_class.reload_mailer_configuration! - expect(ActionMailer::Base).to have_received(:perform_deliveries=).with(true) - expect(ActionMailer::Base).to have_received(:delivery_method=).with(:smtp) - expect(ActionMailer::Base.smtp_settings[:smtp_authentication]).to be_nil - expect(ActionMailer::Base.smtp_settings).to eq(address: 'smtp.example.com', - port: 587, - domain: 'example.com', - authentication: 'plain', - user_name: 'username', - password: 'p4ssw0rd', - enable_starttls_auto: false, - openssl_verify_mode: 'peer', - ssl: true) - end - end - end - - describe '.configure_legacy_action_mailer' do - let(:action_mailer) do - class_double('ActionMailer::Base', - deliveries: []).tap do |mailer| - allow(mailer).to receive(:perform_deliveries=) - allow(mailer).to receive(:delivery_method=) - allow(mailer).to receive(:smtp_settings=) - end - end - let(:settings) do - { 'email_delivery_method' => 'smtp', - 'smtp_address' => 'smtp.example.net', - 'smtp_port' => '25' }.map do |name, value| - API::ParserStruct.new name: name, value: value - end - end - - before do - allow(Settings::Definition) - .to receive(:[]) do |name| - settings.detect { |s| s.name == name } - end - - allow(Settings::Definition) - .to receive(:all_of_prefix) do |prefix| - settings.select { |s| s.name.start_with?(prefix) } - end - - stub_const('ActionMailer::Base', action_mailer) - end - - it 'enables deliveries and configure ActionMailer smtp delivery' do - described_class.send(:configure_legacy_action_mailer) - - expect(action_mailer) - .to have_received(:perform_deliveries=) - .with(true) - expect(action_mailer) - .to have_received(:delivery_method=) - .with(:smtp) - expect(action_mailer) - .to have_received(:smtp_settings=) - .with(address: 'smtp.example.net', - port: '25') - end - end - describe '.configure_cache' do let(:application_config) do Rails::Application::Configuration.new Rails.root @@ -294,17 +75,6 @@ describe OpenProject::Configuration, :settings_reset do expect(application_config.cache_store).to eq([:bar]) end end - - context 'without additional cache store configuration' do - before do - described_class['rails_cache_store'] = nil - end - - it 'does not change the cache store' do - described_class.send(:configure_cache, application_config) - expect(application_config.cache_store).to eq('foo') - end - end end context 'without cache store already set' do diff --git a/spec/mailers/smtp_settings_spec.rb b/spec/mailers/smtp_settings_spec.rb index 652f08a30c..827f9df4b5 100644 --- a/spec/mailers/smtp_settings_spec.rb +++ b/spec/mailers/smtp_settings_spec.rb @@ -53,7 +53,7 @@ describe "SMTP settings" do allow(Setting).to receive(:smtp_ssl?).and_return ssl end - OpenProject::Configuration.reload_mailer_configuration! + Setting.reload_mailer_settings! end def send_mail @@ -166,4 +166,4 @@ describe "SMTP settings" do end end end -end \ No newline at end of file +end diff --git a/spec/models/setting_spec.rb b/spec/models/setting_spec.rb index 68117a1c3c..f1d6bfedae 100644 --- a/spec/models/setting_spec.rb +++ b/spec/models/setting_spec.rb @@ -474,4 +474,121 @@ describe Setting, type: :model do expect(collector).to include 'some name' end end + + describe '.reload_mailer_settings!' do + before do + allow(ActionMailer::Base) + .to receive(:perform_deliveries=) + allow(ActionMailer::Base) + .to receive(:delivery_method=) + end + + context 'without smtp_authentication and without ssl' do + it 'uses the setting values', + with_settings: { + email_delivery_method: :smtp, + smtp_authentication: :none, + smtp_password: 'old', + smtp_address: 'smtp.example.com', + smtp_domain: 'example.com', + smtp_port: 25, + smtp_user_name: 'username', + smtp_enable_starttls_auto: 1, + smtp_ssl: 0 + } do + described_class.reload_mailer_settings! + expect(ActionMailer::Base).to have_received(:perform_deliveries=).with(true) + expect(ActionMailer::Base).to have_received(:delivery_method=).with(:smtp) + expect(ActionMailer::Base.smtp_settings[:smtp_authentication]).to be_nil + expect(ActionMailer::Base.smtp_settings).to eq(address: 'smtp.example.com', + port: 25, + domain: 'example.com', + enable_starttls_auto: true, + openssl_verify_mode: 'peer', + ssl: false) + end + end + + context 'without smtp_authentication and with ssl' do + it 'users the setting values', + with_settings: { + email_delivery_method: :smtp, + smtp_authentication: :none, + smtp_password: 'old', + smtp_address: 'smtp.example.com', + smtp_domain: 'example.com', + smtp_port: 25, + smtp_user_name: 'username', + smtp_enable_starttls_auto: 0, + smtp_ssl: 1 + } do + described_class.reload_mailer_settings! + expect(ActionMailer::Base).to have_received(:perform_deliveries=).with(true) + expect(ActionMailer::Base).to have_received(:delivery_method=).with(:smtp) + expect(ActionMailer::Base.smtp_settings[:smtp_authentication]).to be_nil + expect(ActionMailer::Base.smtp_settings).to eq(address: 'smtp.example.com', + port: 25, + domain: 'example.com', + enable_starttls_auto: false, + openssl_verify_mode: 'peer', + ssl: true) + end + end + + context 'with smtp_authentication and without ssl' do + it 'users the setting values', + with_settings: { + email_delivery_method: :smtp, + smtp_password: 'p4ssw0rd', + smtp_address: 'smtp.example.com', + smtp_domain: 'example.com', + smtp_port: 587, + smtp_user_name: 'username', + smtp_enable_starttls_auto: 1, + smtp_ssl: 0 + } do + described_class.reload_mailer_settings! + expect(ActionMailer::Base).to have_received(:perform_deliveries=).with(true) + expect(ActionMailer::Base).to have_received(:delivery_method=).with(:smtp) + expect(ActionMailer::Base.smtp_settings[:smtp_authentication]).to be_nil + expect(ActionMailer::Base.smtp_settings).to eq(address: 'smtp.example.com', + port: 587, + domain: 'example.com', + authentication: 'plain', + user_name: 'username', + password: 'p4ssw0rd', + enable_starttls_auto: true, + openssl_verify_mode: 'peer', + ssl: false) + end + end + + context 'with smtp_authentication and with ssl' do + it 'users the setting values', + with_settings: { + email_delivery_method: :smtp, + smtp_password: 'p4ssw0rd', + smtp_address: 'smtp.example.com', + smtp_domain: 'example.com', + smtp_port: 587, + smtp_user_name: 'username', + smtp_enable_starttls_auto: 0, + smtp_ssl: 1 + } do + described_class.reload_mailer_settings! + expect(ActionMailer::Base).to have_received(:perform_deliveries=).with(true) + expect(ActionMailer::Base).to have_received(:delivery_method=).with(:smtp) + expect(ActionMailer::Base.smtp_settings[:smtp_authentication]).to be_nil + expect(ActionMailer::Base.smtp_settings).to eq(address: 'smtp.example.com', + port: 587, + domain: 'example.com', + authentication: 'plain', + user_name: 'username', + password: 'p4ssw0rd', + enable_starttls_auto: false, + openssl_verify_mode: 'peer', + ssl: true) + end + end + end end From 9e3c51299aed2b097603de4633ab96e10bfa00dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 18 May 2022 13:50:14 +0200 Subject: [PATCH 26/80] Remove Setting.protocol in favor of static config --- app/helpers/warning_bar_helper.rb | 2 +- app/models/setting/aliases.rb | 11 +- app/models/setting/mail_settings.rb | 2 + .../settings/general_settings/show.html.erb | 1 - config/constants/settings/definitions.rb | 15 ++- config/initializers/session_store.rb | 79 +++++++------ docs/development/localhost-ssl/README.md | 10 +- .../configuration/ssl/README.md | 2 +- lib/tasks/packager.rake | 10 +- .../open_project/configuration/helpers.rb | 6 + .../avatars/patches/avatar_helper_patch.rb | 8 +- .../spec/helpers/avatar_helper_spec.rb | 107 +++++++++--------- modules/bim/lib/open_project/bim/engine.rb | 4 +- .../remember_token.rb | 2 +- 14 files changed, 137 insertions(+), 122 deletions(-) diff --git a/app/helpers/warning_bar_helper.rb b/app/helpers/warning_bar_helper.rb index 40c47fe9f8..14063b5752 100644 --- a/app/helpers/warning_bar_helper.rb +++ b/app/helpers/warning_bar_helper.rb @@ -40,7 +40,7 @@ module WarningBarHelper end def setting_protocol_mismatched? - (request.ssl? && Setting.protocol == 'http') || (!request.ssl? && Setting.protocol == 'https') + request.ssl? != OpenProject::Configuration.secure_connection? end def setting_hostname_mismatched? diff --git a/app/models/setting/aliases.rb b/app/models/setting/aliases.rb index 778f044e19..6bf0524aca 100644 --- a/app/models/setting/aliases.rb +++ b/app/models/setting/aliases.rb @@ -31,10 +31,13 @@ class Setting # Shorthand to common setting aliases to avoid checking values module Aliases ## - # Whether the application is configured to use or force SSL output - # for cookie storage et al. - def https? - Setting.protocol == 'https' || Rails.configuration.force_ssl + # Restore the previous Setting.protocol now replaced by https? + def protocol + if OpenProject::Configuration.secure_connection? + 'https' + else + 'http' + end end end end diff --git a/app/models/setting/mail_settings.rb b/app/models/setting/mail_settings.rb index 5d84b7a3c0..66e9fb8bbe 100644 --- a/app/models/setting/mail_settings.rb +++ b/app/models/setting/mail_settings.rb @@ -44,6 +44,7 @@ class Setting private + # rubocop:disable Metrics/AbcSize def reload_smtp_settings! # Correct smtp settings when using authentication :none authentication = Setting.smtp_authentication.try(:to_sym) @@ -74,5 +75,6 @@ class Setting ActionMailer::Base.smtp_settings[:openssl_verify_mode] = mode unless mode.nil? end end + # rubocop:enable Metrics/AbcSize end end diff --git a/app/views/admin/settings/general_settings/show.html.erb b/app/views/admin/settings/general_settings/show.html.erb index e54cc5c24c..707e490835 100644 --- a/app/views/admin/settings/general_settings/show.html.erb +++ b/app/views/admin/settings/general_settings/show.html.erb @@ -47,7 +47,6 @@ See COPYRIGHT and LICENSE files for more details. <%= t(:label_example) %>: <%= @guessed_host %> -
<%= setting_select :protocol, [['HTTP', 'http'], ['HTTPS', 'https']], container_class: '-xslim' %>
<%= setting_check_box :cache_formatted_text %>
<%= setting_check_box :feeds_enabled, size: 6 %>
<%= setting_text_field :feeds_limit, size: 6, container_class: '-xslim' %>
diff --git a/config/constants/settings/definitions.rb b/config/constants/settings/definitions.rb index 717a602de0..42b85dc76d 100644 --- a/config/constants/settings/definitions.rb +++ b/config/constants/settings/definitions.rb @@ -638,10 +638,6 @@ Settings::Definition.define do add :plain_text_mail, default: false - add :protocol, - default: "http", - allowed: %w[http https] - add :project_gantt_query, default: nil, format: :string @@ -662,8 +658,17 @@ Settings::Definition.define do default: '', writable: false + # Assume we're running in an TLS terminated connection. + # This does not affect HSTS, use +rails_force_ssl+ for that. + add :https, + format: :boolean, + default: Rails.env.production?, + writable: false + + # Enable HTTPS and HSTS add :rails_force_ssl, - default: false, + format: :boolean, + default: Rails.env.production?, writable: false add :registration_footer, diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index d035328f0b..8412c38f44 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -54,7 +54,7 @@ module OpenProject rescue StandardError => e Rails.logger.error( "Failed to determine new `after_expire` value. " + - "Falling back to original value. (#{e.message} at #{caller.first})" + "Falling back to original value. (#{e.message} at #{caller.first})" ) options[:expire_after] @@ -66,52 +66,52 @@ module OpenProject end end -Rails.application.config.after_initialize do - config = OpenProject::Configuration +config = OpenProject::Configuration - # Enforce session storage for testing - if Rails.env.test? - config['session_store'] = :active_record_store - end +# Enforce session storage for testing +if Rails.env.test? + config['session_store'] = :active_record_store +end - session_store = config['session_store'].to_sym - relative_url_root = config['rails_relative_url_root'].presence - - session_options = { - key: config['session_cookie_name'], - httponly: true, - secure: Setting.https?, - path: relative_url_root - } - - if session_store == :cache_store - # env OPENPROJECT_CACHE__STORE__SESSION__USER__TTL__DAYS - session_ttl = config['cache_store_session_user_ttl_days']&.to_i&.days || 3.days - - # Extend session cache entry TTL so that they can stay logged in when their - # session ID cookie's TTL is 'session' where usually the session entry in the - # cache would expire before the session in the browser by default. - session_options[:expire_store_after] = lambda do |session, expire_after| - if session.include? "user_id" # logged-in user - [session_ttl, expire_after].compact.max - else - expire_after # anonymous user - end +session_store = config['session_store'].to_sym +relative_url_root = config['rails_relative_url_root'].presence + +session_options = { + key: config['session_cookie_name'], + httponly: true, + secure: config.secure_connection?, + path: relative_url_root +} + +if session_store == :cache_store + # env OPENPROJECT_CACHE__STORE__SESSION__USER__TTL__DAYS + session_ttl = config['cache_store_session_user_ttl_days']&.to_i&.days || 3.days + + # Extend session cache entry TTL so that they can stay logged in when their + # session ID cookie's TTL is 'session' where usually the session entry in the + # cache would expire before the session in the browser by default. + session_options[:expire_store_after] = lambda do |session, expire_after| + if session.include? "user_id" # logged-in user + [session_ttl, expire_after].compact.max + else + expire_after # anonymous user end + end +end - method = ActionDispatch::Session::CacheStore.instance_method(:write_session) - unless method.to_s.include?("write_session(env, sid, session, options)") - raise( - "The signature for `ActionDispatch::Session::CacheStore.write_session` " + +OpenProject::Application.config.session_store session_store, **session_options + +Rails.application.reloader.to_prepare do + method = ActionDispatch::Session::CacheStore.instance_method(:write_session) + unless method.to_s.include?("write_session(env, sid, session, options)") + raise( + "The signature for `ActionDispatch::Session::CacheStore.write_session` " + "seems to have changed. Please update the " + "`ExpireStoreAfterOption` module (and this check) in #{__FILE__}" - ) - end - - ActionDispatch::Session::CacheStore.prepend OpenProject::ExpireStoreAfterOption + ) end - OpenProject::Application.config.session_store session_store, **session_options + ActionDispatch::Session::CacheStore.prepend OpenProject::ExpireStoreAfterOption ## # We use our own decorated session model to note the user_id @@ -120,4 +120,3 @@ Rails.application.config.after_initialize do # Continue to use marshal serialization to retain symbols and whatnot ActiveRecord::SessionStore::Session.serializer = :marshal end - diff --git a/docs/development/localhost-ssl/README.md b/docs/development/localhost-ssl/README.md index 0c2d262cbd..58464c3e93 100644 --- a/docs/development/localhost-ssl/README.md +++ b/docs/development/localhost-ssl/README.md @@ -102,17 +102,13 @@ Rails.application.config.hosts << 'openproject.example.com' -Then, you will start a REPL console for OpenProject with: `RAILS_ENV=development ./bin/rails console` - -Update the settings for host name and protocol: +Then, you will to set the following settings ```ruby -Setting.protocol = 'https' -Setting.host_name = 'openproject.example.com' +export OPENPROJECT_HTTPS = true +export OPENPROJECT_HOST__NAME = 'openproject.example.com' ``` - - Finally, start your OpenProject development server and Frontend server and access `https://openproject.example.com` in your browser. diff --git a/docs/installation-and-operations/configuration/ssl/README.md b/docs/installation-and-operations/configuration/ssl/README.md index a96683cf51..3338b0a88f 100644 --- a/docs/installation-and-operations/configuration/ssl/README.md +++ b/docs/installation-and-operations/configuration/ssl/README.md @@ -79,7 +79,7 @@ If you're terminating SSL on the outer server, you need to set the `X-Forwarded- -Finally, to let OpenProject know that it should create links with 'https' when no request is available (for example, when sending emails), you need to set the Protocol setting of OpenProject to `https`. You will find this setting on your system settings or via the rails console with `Setting.protocol = 'https'` +Finally, to let OpenProject know that it should create links with 'https' when no request is available (for example, when sending emails), you need to set the Protocol setting of OpenProject to `https`. You can set this configuration by setting the ENV `OPENPROJECT_HTTPS=true`. _1 In the packaged installation this means you selected "no" when asked for SSL in the configuration wizard but at the same time take care of SSL termination elsewhere. This can be a manual Apache setup on the same server (not recommended) or an external server, for instance._ diff --git a/lib/tasks/packager.rake b/lib/tasks/packager.rake index f986680e26..47b0675209 100644 --- a/lib/tasks/packager.rake +++ b/lib/tasks/packager.rake @@ -66,17 +66,17 @@ namespace :packager do Setting.host_name = ENV.fetch('SERVER_HOSTNAME', Setting.host_name) if ENV['SERVER_PROTOCOL_HTTPS_NO_HSTS'] - # Allow setting only the Setting.protocol without enabling FORCE__SSL - # due to external proxy configuration - Setting.protocol = 'https' + # Allow setting only HTTPS setting without enabling FORCE__SSL + # due to external proxy configuration. This avoids activation of HSTS headers. + shell_setup(['config:set', "OPENPROJECT_HTTPS=true"]) shell_setup(['config:unset', "OPENPROJECT_RAILS__FORCE__SSL"]) elsif ENV['SERVER_PROTOCOL_FORCE_HTTPS'] || ENV.fetch('SERVER_PROTOCOL', Setting.protocol) == 'https' # Allow overriding the protocol setting from ENV # to allow instances where SSL is terminated earlier to respect that setting - Setting.protocol = 'https' + shell_setup(['config:set', "OPENPROJECT_HTTPS=true"]) shell_setup(['config:set', "OPENPROJECT_RAILS__FORCE__SSL=true"]) else - Setting.protocol = 'http' + shell_setup(['config:set', "OPENPROJECT_HTTPS=false"]) shell_setup(['config:unset', "OPENPROJECT_RAILS__FORCE__SSL"]) end diff --git a/lib_static/open_project/configuration/helpers.rb b/lib_static/open_project/configuration/helpers.rb index d1218d30fa..2b6094bc24 100644 --- a/lib_static/open_project/configuration/helpers.rb +++ b/lib_static/open_project/configuration/helpers.rb @@ -32,6 +32,12 @@ module OpenProject # To be included into OpenProject::Configuration in order to provide # helper methods for easier access to certain configuration options. module Helpers + ## + # Are we behind a TLS terminated session? + def secure_connection? + https? || rails_force_ssl? + end + def direct_uploads return false unless direct_uploads_supported? diff --git a/modules/avatars/lib/open_project/avatars/patches/avatar_helper_patch.rb b/modules/avatars/lib/open_project/avatars/patches/avatar_helper_patch.rb index ef2dce2334..6a2eb791bf 100644 --- a/modules/avatars/lib/open_project/avatars/patches/avatar_helper_patch.rb +++ b/modules/avatars/lib/open_project/avatars/patches/avatar_helper_patch.rb @@ -112,10 +112,10 @@ AvatarHelper.class_eval do end def default_gravatar_options - options = { secure: Setting.protocol == 'https' } - options[:default] = OpenProject::Configuration.gravatar_fallback_image - - options + { + secure: OpenProject::Configuration.secure_connection?, + default: OpenProject::Configuration.gravatar_fallback_image + } end ## diff --git a/modules/avatars/spec/helpers/avatar_helper_spec.rb b/modules/avatars/spec/helpers/avatar_helper_spec.rb index 504fa90410..326f50aedb 100644 --- a/modules/avatars/spec/helpers/avatar_helper_spec.rb +++ b/modules/avatars/spec/helpers/avatar_helper_spec.rb @@ -60,11 +60,12 @@ describe AvatarHelper, type: :helper, with_settings: { protocol: 'http' } do context 'when enabled' do let(:enable_gravatars) { true } let(:enable_local_avatars) { true } - it "should return the image attached to the user" do + + it "returns the image attached to the user" do expect(helper.avatar(user)).to be_html_eql(expected_user_avatar_tag(user)) end - it "should return the gravatar image if no image uploaded for the user" do + it "returns the gravatar image if no image uploaded for the user" do allow(user).to receive(:local_avatar_attachment).and_return nil expect(helper.avatar(user)).to be_html_eql(expected_user_avatar_tag(user)) @@ -74,7 +75,8 @@ describe AvatarHelper, type: :helper, with_settings: { protocol: 'http' } do context 'when gravatar disabled' do let(:enable_gravatars) { false } let(:enable_local_avatars) { true } - it "should return blank if image attached to the user but gravatars disabled" do + + it "returns blank if image attached to the user but gravatars disabled" do expect(helper.avatar(user)).to be_html_eql(expected_user_avatar_tag(user)) end end @@ -83,7 +85,7 @@ describe AvatarHelper, type: :helper, with_settings: { protocol: 'http' } do let(:enable_gravatars) { false } let(:enable_local_avatars) { false } - it "should return blank" do + it "returns blank" do expect(helper.avatar(user)).to be_html_eql(expected_user_avatar_tag(user)) end end @@ -93,11 +95,12 @@ describe AvatarHelper, type: :helper, with_settings: { protocol: 'http' } do context 'when enabled' do let(:enable_gravatars) { true } let(:enable_local_avatars) { true } - it "should return the url to the image attached to the user" do + + it "returns the url to the image attached to the user" do expect(helper.avatar_url(user)).to eq(local_expected_url(user)) end - it "should return the gravatar url if no image uploaded for the user" do + it "returns the gravatar url if no image uploaded for the user" do allow(user).to receive(:local_avatar_attachment).and_return nil expect(helper.avatar_url(user)).to eq(gravatar_expected_url(mail_digest)) @@ -107,7 +110,8 @@ describe AvatarHelper, type: :helper, with_settings: { protocol: 'http' } do context 'when gravatar disabled' do let(:enable_gravatars) { false } let(:enable_local_avatars) { true } - it "should return the url if image attached to the user but gravatars disabled" do + + it "returns the url if image attached to the user but gravatars disabled" do expect(helper.avatar_url(user)).to eq(local_expected_url(user)) end end @@ -116,71 +120,70 @@ describe AvatarHelper, type: :helper, with_settings: { protocol: 'http' } do let(:enable_gravatars) { false } let(:enable_local_avatars) { false } - it "should return blank" do + it "returns blank" do expect(helper.avatar_url(user)).to eq '' end end end - describe 'gravatar' do - context 'when enabled' do - let(:enable_gravatars) { true } - let(:enable_local_avatars) { false } - describe 'ssl dependent on protocol settings' do - context 'with https protocol', with_settings: { protocol: 'https' } do - it "should be set to secure if protocol is 'https'" do - expect(helper.default_gravatar_options[:secure]).to be true - end - end + context 'when gravatar enabled' do + let(:enable_gravatars) { true } + let(:enable_local_avatars) { false } - context 'with http protocol', with_settings: { protocol: 'http' } do - it "should be set to unsecure if protocol is 'http'" do - expect(helper.default_gravatar_options[:secure]).to be false - end + describe 'ssl dependent on protocol settings' do + context 'with https protocol', with_config: { https: true } do + it "is set to secure if protocol is 'https'" do + expect(helper.default_gravatar_options[:secure]).to be true end end - context 'with http', with_settings: { protocol: 'http' } do - it 'should return a gravatar image tag if a user is provided' do - expect(helper.avatar(user)).to be_html_eql(expected_user_avatar_tag(user)) - end - - it 'should return a gravatar url if a user is provided' do - expect(helper.avatar_url(user)).to eq(gravatar_expected_url(mail_digest)) + context 'with http protocol', with_config: { https: false } do + it "is set to unsecure if protocol is 'http'" do + expect(helper.default_gravatar_options[:secure]).to be false end end + end - context 'with https', with_settings: { protocol: 'https' } do - it 'should return a gravatar image tag with ssl if the request was ssl required' do - expect(helper.avatar(user)).to be_html_eql(expected_user_avatar_tag(user)) - end + context 'with http', with_config: { https: false } do + it 'returns a gravatar image tag if a user is provided' do + expect(helper.avatar(user)).to be_html_eql(expected_user_avatar_tag(user)) + end - it 'should return a gravatar image tag with ssl if the request was ssl required' do - expect(helper.avatar_url(user)).to eq(gravatar_expected_url(mail_digest, ssl: true)) - end + it 'returns a gravatar url if a user is provided' do + expect(helper.avatar_url(user)).to eq(gravatar_expected_url(mail_digest)) end + end - it 'should return an empty string if a non parsable (e-mail) string is provided' do - expect(helper.avatar('just the name')).to eq('') + context 'with https', with_config: { https: true } do + it 'returns a gravatar image tag without ssl if the request was no ssl required' do + expect(helper.avatar(user)).to be_html_eql(expected_user_avatar_tag(user)) end - it 'should return an empty string if nil is provided' do - expect(helper.avatar(nil)).to eq('') + it 'returns a gravatar image tag with ssl if the request was ssl required' do + expect(helper.avatar_url(user)).to eq(gravatar_expected_url(mail_digest, ssl: true)) end + end - it 'should return an empty string if a parsable e-mail with default avatar is provided' do - mail = '' + it 'returns an empty string if a non parsable (e-mail) string is provided' do + expect(helper.avatar('just the name')).to eq('') + end - expect(helper.avatar(mail)).to eq('') - end + it 'returns an empty string if nil is provided' do + expect(helper.avatar(nil)).to eq('') + end - it 'should return an empty string if a non parsable (e-mail) string is provided' do - expect(helper.avatar_url('just the name')).to eq('') - end + it 'returns an empty string if a parsable e-mail with default avatar is provided' do + mail = '' - it 'should return an empty string if nil is provided' do - expect(helper.avatar_url(nil)).to eq('') - end + expect(helper.avatar(mail)).to eq('') + end + + it 'returns an empty string if a non parsable (e-mail) string url is provided' do + expect(helper.avatar_url('just the name')).to eq('') + end + + it 'returns an empty string if nil url is provided' do + expect(helper.avatar_url(nil)).to eq('') end end @@ -188,11 +191,11 @@ describe AvatarHelper, type: :helper, with_settings: { protocol: 'http' } do let(:enable_gravatars) { false } let(:enable_local_avatars) { false } - it 'should return an empty string if gravatar is disabled' do + it 'returns an empty string for avatar if gravatar is disabled' do expect(helper.avatar(user)).to be_html_eql(expected_user_avatar_tag(user)) end - it 'should return an empty string if gravatar is disabled' do + it 'returns an empty string for avatar_url if gravatar is disabled' do expect(helper.avatar_url(user)).to eq('') end end diff --git a/modules/bim/lib/open_project/bim/engine.rb b/modules/bim/lib/open_project/bim/engine.rb index ff9cfb1c2e..e08f468a5b 100644 --- a/modules/bim/lib/open_project/bim/engine.rb +++ b/modules/bim/lib/open_project/bim/engine.rb @@ -203,9 +203,11 @@ module OpenProject::Bim config.to_prepare do Doorkeeper.configuration.scopes.add(:bcf_v2_1) + # rubocop:disable Lint/ConstantDefinitionInBlock module OpenProject::Authentication::Scope - BCF_V2_1 = :bcf_v2_1 # rubocop:disable Lint/ConstantDefinitionInBlock + BCF_V2_1 = :bcf_v2_1 end + # rubocop:enable Lint/ConstantDefinitionInBlock OpenProject::Authentication.update_strategies(OpenProject::Authentication::Scope::BCF_V2_1, store: false) do |_strategies| diff --git a/modules/two_factor_authentication/app/controllers/concerns/two_factor_authentication/remember_token.rb b/modules/two_factor_authentication/app/controllers/concerns/two_factor_authentication/remember_token.rb index dfc4c0b4ad..7ef78c7773 100644 --- a/modules/two_factor_authentication/app/controllers/concerns/two_factor_authentication/remember_token.rb +++ b/modules/two_factor_authentication/app/controllers/concerns/two_factor_authentication/remember_token.rb @@ -29,7 +29,7 @@ module ::TwoFactorAuthentication value: new_token!(@authenticated_user), httponly: true, expires: remember_2fa_days.days.from_now, - secure: Setting.protocol == 'https' + secure: OpenProject::Configuration.secure_connection? } end From c54ac51f90360cea87ab436d1f7072f55d983ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 18 May 2022 16:23:03 +0200 Subject: [PATCH 27/80] Replace to_prepare with after_initialize for one-time calls --- config/initializers/rack_timeout.rb | 2 +- config/initializers/secure_headers.rb | 2 +- config/initializers/sentry.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/initializers/rack_timeout.rb b/config/initializers/rack_timeout.rb index 9d93c6a8b7..17d1f34c93 100644 --- a/config/initializers/rack_timeout.rb +++ b/config/initializers/rack_timeout.rb @@ -14,7 +14,7 @@ if OpenProject::Configuration.web_workers >= 2 term_on_timeout: 1 # shut down worker (gracefully) right away on timeout to be restarted ) - Rails.application.reloader.to_prepare do + Rails.application.config.after_initialize do # remove default logger (logging uninteresting extra info with each not timed out request) Rack::Timeout.unregister_state_change_observer(:logger) diff --git a/config/initializers/secure_headers.rb b/config/initializers/secure_headers.rb index 99b259db0a..82dc8c4112 100644 --- a/config/initializers/secure_headers.rb +++ b/config/initializers/secure_headers.rb @@ -1,5 +1,5 @@ # rubocop:disable Lint/PercentStringArray -Rails.application.reloader.to_prepare do +Rails.application.config.after_initialize do SecureHeaders::Configuration.default do |config| config.cookies = { secure: true, diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 0c18a359dc..fec6f16a1b 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -1,5 +1,5 @@ -if OpenProject::Logging::SentryLogger.enabled? - Rails.application.reloader.to_prepare do +Rails.application.config.after_initialize do + if OpenProject::Logging::SentryLogger.enabled? Sentry.init do |config| config.dsn = OpenProject::Logging::SentryLogger.sentry_dsn config.breadcrumbs_logger = OpenProject::Configuration.sentry_breadcrumb_loggers.map(&:to_sym) From 87b454e093e04d6a9e9f72ee96edaa065095fd27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 18 May 2022 16:24:09 +0200 Subject: [PATCH 28/80] Remove bcrypt override deprecation --- config/initializers/bcrypt.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/bcrypt.rb b/config/initializers/bcrypt.rb index da087626ff..fb6536f271 100644 --- a/config/initializers/bcrypt.rb +++ b/config/initializers/bcrypt.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -if OpenProject::Configuration.override_bcrypt_cost_factor? +if OpenProject::Configuration.override_bcrypt_cost_factor.present? cost_factor = OpenProject::Configuration.override_bcrypt_cost_factor.to_i current = BCrypt::Engine.cost From b783b775f73a86238051fc6c2f6bbb708aebdf20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 18 May 2022 16:39:48 +0200 Subject: [PATCH 29/80] Bump rails to 7.0.3 --- Gemfile | 8 +--- Gemfile.lock | 123 ++++++++++++++++++++++++++------------------------- 2 files changed, 64 insertions(+), 67 deletions(-) diff --git a/Gemfile b/Gemfile index b2506d78a5..01f5b0932e 100644 --- a/Gemfile +++ b/Gemfile @@ -34,7 +34,7 @@ gem 'actionpack-xml_parser', '~> 2.0.0' gem 'activemodel-serializers-xml', '~> 1.0.1' gem 'activerecord-import', '~> 1.4.0' gem 'activerecord-session_store', '~> 2.0.0' -gem 'rails', '~> 6.1.5', '>= 6.1.5.1' +gem 'rails', '~> 7.0.3' gem 'responders', '~> 3.0' gem 'ffi', '~> 1.15' @@ -62,12 +62,6 @@ gem 'typed_dag', '~> 2.0.2' gem 'addressable', '~> 2.8.0' -# Needed to make rails 6.x work with ruby 3.1, can be dropped -# after migrated to rails 7 (see https://stackoverflow.com/a/70500221) -gem 'net-smtp', '~> 0.3.1', require: false -gem 'net-pop', '~> 0.1.1', require: false -gem 'net-imap', '~> 0.2.3', require: false - # Remove whitespace from model input gem "auto_strip_attributes", "~> 2.5" diff --git a/Gemfile.lock b/Gemfile.lock index e679efdb17..fb22d83248 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -188,59 +188,66 @@ GEM remote: https://rubygems.org/ specs: Ascii85 (1.1.0) - actioncable (6.1.6) - actionpack (= 6.1.6) - activesupport (= 6.1.6) + actioncable (7.0.3) + actionpack (= 7.0.3) + activesupport (= 7.0.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.6) - actionpack (= 6.1.6) - activejob (= 6.1.6) - activerecord (= 6.1.6) - activestorage (= 6.1.6) - activesupport (= 6.1.6) + actionmailbox (7.0.3) + actionpack (= 7.0.3) + activejob (= 7.0.3) + activerecord (= 7.0.3) + activestorage (= 7.0.3) + activesupport (= 7.0.3) mail (>= 2.7.1) - actionmailer (6.1.6) - actionpack (= 6.1.6) - actionview (= 6.1.6) - activejob (= 6.1.6) - activesupport (= 6.1.6) + net-imap + net-pop + net-smtp + actionmailer (7.0.3) + actionpack (= 7.0.3) + actionview (= 7.0.3) + activejob (= 7.0.3) + activesupport (= 7.0.3) mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp rails-dom-testing (~> 2.0) - actionpack (6.1.6) - actionview (= 6.1.6) - activesupport (= 6.1.6) - rack (~> 2.0, >= 2.0.9) + actionpack (7.0.3) + actionview (= 7.0.3) + activesupport (= 7.0.3) + rack (~> 2.0, >= 2.2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) actionpack-xml_parser (2.0.1) actionpack (>= 5.0) railties (>= 5.0) - actiontext (6.1.6) - actionpack (= 6.1.6) - activerecord (= 6.1.6) - activestorage (= 6.1.6) - activesupport (= 6.1.6) + actiontext (7.0.3) + actionpack (= 7.0.3) + activerecord (= 7.0.3) + activestorage (= 7.0.3) + activesupport (= 7.0.3) + globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (6.1.6) - activesupport (= 6.1.6) + actionview (7.0.3) + activesupport (= 7.0.3) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.6) - activesupport (= 6.1.6) + activejob (7.0.3) + activesupport (= 7.0.3) globalid (>= 0.3.6) - activemodel (6.1.6) - activesupport (= 6.1.6) + activemodel (7.0.3) + activesupport (= 7.0.3) activemodel-serializers-xml (1.0.2) activemodel (> 5.x) activesupport (> 5.x) builder (~> 3.1) - activerecord (6.1.6) - activemodel (= 6.1.6) - activesupport (= 6.1.6) + activerecord (7.0.3) + activemodel (= 7.0.3) + activesupport (= 7.0.3) activerecord-import (1.4.0) activerecord (>= 4.2) activerecord-nulldb-adapter (0.8.0) @@ -251,19 +258,18 @@ GEM multi_json (~> 1.11, >= 1.11.2) rack (>= 2.0.8, < 3) railties (>= 5.2.4.1) - activestorage (6.1.6) - actionpack (= 6.1.6) - activejob (= 6.1.6) - activerecord (= 6.1.6) - activesupport (= 6.1.6) + activestorage (7.0.3) + actionpack (= 7.0.3) + activejob (= 7.0.3) + activerecord (= 7.0.3) + activesupport (= 7.0.3) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.6) + activesupport (7.0.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - zeitwerk (~> 2.3) acts_as_list (1.0.4) activerecord (>= 4.2) acts_as_tree (2.9.1) @@ -755,21 +761,20 @@ GEM rack_session_access (0.2.0) builder (>= 2.0.0) rack (>= 1.0.0) - rails (6.1.6) - actioncable (= 6.1.6) - actionmailbox (= 6.1.6) - actionmailer (= 6.1.6) - actionpack (= 6.1.6) - actiontext (= 6.1.6) - actionview (= 6.1.6) - activejob (= 6.1.6) - activemodel (= 6.1.6) - activerecord (= 6.1.6) - activestorage (= 6.1.6) - activesupport (= 6.1.6) + rails (7.0.3) + actioncable (= 7.0.3) + actionmailbox (= 7.0.3) + actionmailer (= 7.0.3) + actionpack (= 7.0.3) + actiontext (= 7.0.3) + actionview (= 7.0.3) + activejob (= 7.0.3) + activemodel (= 7.0.3) + activerecord (= 7.0.3) + activestorage (= 7.0.3) + activesupport (= 7.0.3) bundler (>= 1.15.0) - railties (= 6.1.6) - sprockets-rails (>= 2.0.0) + railties (= 7.0.3) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -782,12 +787,13 @@ GEM rails-i18n (7.0.3) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (6.1.6) - actionpack (= 6.1.6) - activesupport (= 6.1.6) + railties (7.0.3) + actionpack (= 7.0.3) + activesupport (= 7.0.3) method_source rake (>= 12.2) thor (~> 1.0) + zeitwerk (~> 2.5) rainbow (3.1.1) rake (13.0.6) rb-fsevent (0.11.1) @@ -1066,10 +1072,7 @@ DEPENDENCIES mini_magick (~> 4.11.0) multi_json (~> 1.15.0) my_page! - net-imap (~> 0.2.3) net-ldap (~> 0.17.0) - net-pop (~> 0.1.1) - net-smtp (~> 0.3.1) nokogiri (~> 1.13.4) oj (~> 3.13.0) okcomputer (~> 1.18.1) @@ -1120,7 +1123,7 @@ DEPENDENCIES rack-test (~> 1.1.0) rack-timeout (~> 0.6.0) rack_session_access - rails (~> 6.1.5, >= 6.1.5.1) + rails (~> 7.0.3) rails-controller-testing (~> 1.0.2) rails-i18n (~> 7.0.0) rdoc (>= 2.4.2) From 11aeee295c46d146d095bd5bf5eda48ad58a2cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 18 May 2022 16:39:54 +0200 Subject: [PATCH 30/80] Remove autoloader option --- config/application.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/application.rb b/config/application.rb index f9d2d84283..9dddef095f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -85,7 +85,6 @@ module OpenProject # http://stackoverflow.com/questions/4590229 config.middleware.use Rack::TempfileReaper - config.autoloader = :zeitwerk # Custom directories with classes and modules you want to be autoloadable. config.enable_dependency_loading = true config.paths.add Rails.root.join('lib').to_s, eager_load: true From 21e1c5353dfaf71b1315b1a665edb7cc573227b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 18 May 2022 16:40:01 +0200 Subject: [PATCH 31/80] Fix attribute definition --- app/models/concerns/tableless.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/concerns/tableless.rb b/app/models/concerns/tableless.rb index 7861b206d6..cb99c40d3a 100644 --- a/app/models/concerns/tableless.rb +++ b/app/models/concerns/tableless.rb @@ -42,15 +42,15 @@ module Tableless @columns_hash ||= Hash.new # From active_record/attributes.rb - attributes_to_define_after_schema_loads.each do |name, (type, options)| + attributes_to_define_after_schema_loads.each do |name, (type, default)| if type.is_a?(Symbol) - type = ActiveRecord::Type.lookup(type, **options.except(:default)) + type = ActiveRecord::Type.lookup(type, default) end - define_attribute(name, type, **options.slice(:default)) + define_attribute(name, type, default: default) # Improve Model#inspect output - @columns_hash[name.to_s] = ActiveRecord::ConnectionAdapters::Column.new(name.to_s, options[:default]) + @columns_hash[name.to_s] = ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default) end end end From 92fce171a3d676fb4380414a2a5a5d6d76c5a72b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 18 May 2022 21:33:40 +0200 Subject: [PATCH 32/80] Add sprockets-rails for reinstating rake task --- Gemfile | 2 +- Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 01f5b0932e..fa3984b88b 100644 --- a/Gemfile +++ b/Gemfile @@ -164,7 +164,7 @@ end gem 'i18n-js', '~> 3.9.0' gem 'rails-i18n', '~> 7.0.0' -gem 'sprockets', '~> 3.7.0' +gem 'sprockets-rails', '~> 3.4.2' gem 'puma', '~> 5.6' gem 'rack-timeout', '~> 0.6.0', require: "rack/timeout/base" diff --git a/Gemfile.lock b/Gemfile.lock index fb22d83248..7a5e30cb74 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1156,7 +1156,7 @@ DEPENDENCIES shoulda-matchers (~> 5.0) spring spring-commands-rspec - sprockets (~> 3.7.0) + sprockets-rails (~> 3.4.2) stackprof stringex (~> 2.8.5) structured_warnings (~> 0.4.0) From d0dd2bddcaea9d7ef6674460373ff8e85c7743e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 18 May 2022 21:56:06 +0200 Subject: [PATCH 33/80] Bump rspec-rails to rc to fix view spec --- Gemfile | 2 +- Gemfile.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index fa3984b88b..99811d10db 100644 --- a/Gemfile +++ b/Gemfile @@ -210,7 +210,7 @@ group :test do gem 'rack_session_access' gem 'rspec', '~> 3.11.0' # also add to development group, so "spec" rake task gets loaded - gem 'rspec-rails', '~> 5.1.0', group: :development + gem 'rspec-rails', '6.0.0.rc1', group: :development # Retry failures within the same environment gem 'retriable', '~> 3.1.1' diff --git a/Gemfile.lock b/Gemfile.lock index 7a5e30cb74..5d42f9a07c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -840,14 +840,14 @@ GEM rspec-mocks (3.11.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.11.0) - rspec-rails (5.1.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) + rspec-rails (6.0.0.rc1) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.11) + rspec-expectations (~> 3.11) + rspec-mocks (~> 3.11) + rspec-support (~> 3.11) rspec-retry (0.6.2) rspec-core (> 3.3) rspec-support (3.11.0) @@ -1135,7 +1135,7 @@ DEPENDENCIES roar (~> 1.1.0) rouge (~> 3.28.0) rspec (~> 3.11.0) - rspec-rails (~> 5.1.0) + rspec-rails (= 6.0.0.rc1) rspec-retry (~> 0.6.1) rubocop rubocop-rails From 819f3d83ba3d85a100b607ae943d517a27cf4c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 18 May 2022 21:58:45 +0200 Subject: [PATCH 34/80] Remove erb suffix --- spec/views/common/validation_error.html.erb_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/views/common/validation_error.html.erb_spec.rb b/spec/views/common/validation_error.html.erb_spec.rb index c058b1206b..6581bbf576 100644 --- a/spec/views/common/validation_error.html.erb_spec.rb +++ b/spec/views/common/validation_error.html.erb_spec.rb @@ -34,7 +34,7 @@ describe 'common/_validation_error', type: :view do before do view.content_for(:error_details, 'Clear this!') - render partial: 'common/validation_error.html.erb', + render partial: 'common/validation_error', locals: { error_messages: error_message, classes: 'Foo', object_name: 'Test' } From ddce9725cb3b21948e9eb7372b1717e8339b361b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 18 May 2022 22:03:19 +0200 Subject: [PATCH 35/80] Remove mysql-specific reference --- .../work_packages/auto_completes_controller.rb | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/app/controllers/work_packages/auto_completes_controller.rb b/app/controllers/work_packages/auto_completes_controller.rb index 93a972290d..620aee2824 100644 --- a/app/controllers/work_packages/auto_completes_controller.rb +++ b/app/controllers/work_packages/auto_completes_controller.rb @@ -81,19 +81,6 @@ class WorkPackages::AutoCompletesController < ::ApplicationController end def work_package_scope - scope = WorkPackage.all - - # The filter on subject in combination with the ORDER BY on id - # seems to trip MySql's usage of indexes on the order statement - # I haven't seen similar problems on postgresql but there might be as the - # data at hand was not very large. - # - # For MySql we are therefore helping the DB optimizer to use the correct index - - if ActiveRecord::Base.connection_config[:adapter] == 'mysql2' - scope = scope.from("#{WorkPackage.table_name} USE INDEX(PRIMARY)") - end - - scope + WorkPackage.all end end From 1e84bdc0be0bdc41a8be96bf10b0332b47d2bed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 19 May 2022 08:25:45 +0200 Subject: [PATCH 36/80] Specify format in js.erb partial render --- modules/backlogs/app/views/shared/_backlogs_header.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/backlogs/app/views/shared/_backlogs_header.html.erb b/modules/backlogs/app/views/shared/_backlogs_header.html.erb index c3d4b0672e..7fdce194dd 100644 --- a/modules/backlogs/app/views/shared/_backlogs_header.html.erb +++ b/modules/backlogs/app/views/shared/_backlogs_header.html.erb @@ -32,5 +32,5 @@ See COPYRIGHT and LICENSE files for more details. <% end %> <%= nonced_javascript_tag do %> - <%= render(partial: 'shared/server_variables.js.erb') %> + <%= render(partial: 'shared/server_variables', formats: [:js]) %> <% end %> From 35ec946ae63e8101808dfda0d3245063360b8aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 19 May 2022 09:11:12 +0200 Subject: [PATCH 37/80] Restrict type after principal filter In rails 7, this otherwise causes an STI error --- modules/costs/app/helpers/costlog_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/costs/app/helpers/costlog_helper.rb b/modules/costs/app/helpers/costlog_helper.rb index f0f2acab93..a27c3dfc43 100644 --- a/modules/costs/app/helpers/costlog_helper.rb +++ b/modules/costs/app/helpers/costlog_helper.rb @@ -38,8 +38,9 @@ module CostlogHelper end def user_collection_for_select_options(_options = {}) - User + Principal .possible_assignee(@project) + .where(type: 'User') .map { |t| [t.name, t.id] } end From 2e9312ee22380d785ef2f33ab497f6b8104aa762 Mon Sep 17 00:00:00 2001 From: Henriette Darge Date: Thu, 19 May 2022 14:20:02 +0200 Subject: [PATCH 38/80] Update text-field.component.ts Avoid `undefined` as a value being set because Angular sometimes fires this method on initialisation --- .../src/app/spot/components/text-field/text-field.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/spot/components/text-field/text-field.component.ts b/frontend/src/app/spot/components/text-field/text-field.component.ts index c5616b1a00..c3f84f6fac 100644 --- a/frontend/src/app/spot/components/text-field/text-field.component.ts +++ b/frontend/src/app/spot/components/text-field/text-field.component.ts @@ -50,7 +50,7 @@ export class SpotTextFieldComponent implements ControlValueAccessor { } writeValue(value:string) { - this.value = value; + this.value = value || ''; } onChange = (_:string):void => {}; From e8d872e3893ae721fc017d5f5bdef8cee25297e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 19 May 2022 16:32:23 +0200 Subject: [PATCH 39/80] Improve ENV documentation for SAML --- .../authentication/saml/README.md | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/docs/system-admin-guide/authentication/saml/README.md b/docs/system-admin-guide/authentication/saml/README.md index e5809ab097..7d841848c6 100644 --- a/docs/system-admin-guide/authentication/saml/README.md +++ b/docs/system-admin-guide/authentication/saml/README.md @@ -48,13 +48,16 @@ The following is an exemplary file with a set of common settings: ```yaml saml: + # Name of the provider, leave this at saml unless you use multiple providers name: "saml" + # The name that will be display in the login button display_name: "My SSO" # Use the default SAML icon icon: "auth_provider-saml.png" - # omniauth-saml config + # The callback within OpenProject that your idP should redirect to assertion_consumer_service_url: "https:///auth/saml/callback" + # The SAML issuer string that OpenProject will call your idP with issuer: "https://" # IF your SSL certificate on your SSO is not trusted on this machine, you need to add it here in ONE line @@ -65,10 +68,10 @@ saml: # Either `idp_cert` or `idp_cert_fingerprint` must be present! idp_cert_fingerprint: "E7:91:B2:E1:..." - # Replace with your single sign on URL + # Replace with your redirect flow single sign on URL # For example: "https://sso.example.com/saml/singleSignOn" idp_sso_target_url: "" - # Replace with your single sign out URL + # Replace with your redirect flow single sign out URL # or comment out # For example: "https://sso.example.com/saml/proxySingleLogout" idp_slo_target_url: "" @@ -94,10 +97,39 @@ As with [all the rest of the OpenProject configuration settings](../../../instal E.g. ```bash -OPENPROJECT_SAML_MY__SAML_NAME="your-provider-name" -OPENPROJECT_SAML_MY__SAML_DISPLAY__NAME="My SAML provider" -... -OPENPROJECT_SAML_MY__SAML_ATTRIBUTE__STATEMENTS_ADMIN="['openproject-isadmin']" +# Name of the provider, leave this at saml unless you use multiple providers +OPENPROJECT_SAML_SAML_NAME="saml" + +# The name that will be display in the login button +OPENPROJECT_SAML_SAML_DISPLAY__NAME=">Name of the login button>" + +# The callback within OpenProject that your idP should redirect to +OPENPROJECT_SAML_SAML_ASSERTION__CONSUMER__SERVICE__URL="https:///auth/saml/callback" + +# The SAML issuer string that OpenProject will call your idP with +OPENPROJECT_SAML_SAML_ISSUER="https://" + +# IF your SSL certificate on your SSO is not trusted on this machine, you need to add it here in ONE line +### one liner to generate certificate in ONE line +### awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' +#idp_cert: "-----BEGIN CERTIFICATE-----\n ..... SSL CERTIFICATE HERE ...-----END CERTIFICATE-----\n" +# Otherwise, the certificate fingerprint must be added +# Either `OPENPROJECT_SAML_SAML_IDP__CERT` or `OPENPROJECT_SAML_SAML_IDP__CERT__FINGERPRINT` must be present! +OPENPROJECT_SAML_SAML_IDP__CERT="-----BEGIN CERTIFICATE----------END CERTIFICATE-----" +OPENPROJECT_SAML_SAML_IDP__CERT__FINGERPRINT="da:39:a3:ee:5e:6b:4b:0d:32:55:bf:ef:95:60:18:90:af:d8:07:09" +# Replace with your single sign on URL +OPENPROJECT_SAML_SAML_IDP__SSO__TARGET__URL="https:///application/saml/vjdyzjls/sso/binding/post/" + +# (Optinal) Replace with your redirect flow single sign out URL that we should redirect to +OPENPROJECT_SAML_SAML_IDP__SLO__TARGET__URL="" + +# +# Which SAMLAttribute we should look for for the corresponding attributes of OpenProject +# can be a string or URI/URN depending on our idP format +OPENPROJECT_SAML_SAML_ATTRIBUTE__STATEMENTS_EMAIL="mail" +OPENPROJECT_SAML_SAML_ATTRIBUTE__STATEMENTS_LOGIN="mail" +OPENPROJECT_SAML_SAML_ATTRIBUTE__STATEMENTS_FIRST__NAME="givenName" +OPENPROJECT_SAML_SAML_ATTRIBUTE__STATEMENTS_LAST__NAME="sn" ``` Please note that every underscore (`_`) in the original configuration key has to be replaced by a duplicate underscore From 7efd14a792e9577353688b701ed8da7f3d1686e5 Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Thu, 19 May 2022 18:10:27 +0200 Subject: [PATCH 40/80] add documentation to API::RootApi.authorize helper --- lib/api/root_api.rb | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/api/root_api.rb b/lib/api/root_api.rb index 97f9866caf..6a87e1706e 100644 --- a/lib/api/root_api.rb +++ b/lib/api/root_api.rb @@ -46,7 +46,7 @@ module API use OpenProject::Authentication::Manager helpers API::Caching::Helpers - helpers do + module Helpers def current_user User.current end @@ -116,11 +116,30 @@ module API current_user && (current_user.admin? || !current_user.anonymous?) end + # Checks that the current user has the given permission or raise + # {API::Errors::Unauthorized}. + # + # @param permission [String] the permission name + # + # @param context [Project, Array, nil] can be: + # * a project : returns true if user is allowed to do the specified + # action on this project + # * a group of projects : returns true if user is allowed on every + # project + # * +nil+ with +options[:global]+ set: check if user has at least one + # role allowed for this action, or falls back to Non Member / + # Anonymous permissions depending if the user is logged + # + # @param global [Boolean] when +true+ and with +context+ set to +nil+: + # checks that the current user is allowed to do the specified action on + # any project + # + # @raise [API::Errors::Unauthorized] when permission is not met def authorize(permission, context: nil, global: false, user: current_user, &block) auth_service = AuthorizationService.new(permission, - context: context, - global: global, - user: user) + context:, + global:, + user:) authorize_by_with_raise auth_service, &block end @@ -184,6 +203,8 @@ module API end end + helpers Helpers + def self.auth_headers lambda do header = OpenProject::Authentication::WWWAuthenticate From f83d4f0b47a971dffa10fa79253ceca013f6823a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 19 May 2022 20:18:53 +0200 Subject: [PATCH 41/80] Use subselect instead of merging scopes --- lib/api/v3/utilities/endpoints/index.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/api/v3/utilities/endpoints/index.rb b/lib/api/v3/utilities/endpoints/index.rb index 2333fad71c..543ab6d53a 100644 --- a/lib/api/v3/utilities/endpoints/index.rb +++ b/lib/api/v3/utilities/endpoints/index.rb @@ -77,7 +77,7 @@ module API private def render_success(query, params, self_path, base_scope) - results = merge_scopes(base_scope, query.results) + results = apply_scope_constraint(base_scope, query.results) if paginated_representer? render_paginated_success(results, query, params, self_path) @@ -159,11 +159,11 @@ module API end end - def merge_scopes(scope_a, scope_b) - if scope_a.is_a? Class - scope_b + def apply_scope_constraint(constraint, result_scope) + if constraint.is_a?(Class) + result_scope else - scope_a.merge(scope_b) + result_scope.where id: constraint.select(:id) end end end From 797bc9f08e997234619f5dc77cafb8fae2a6bd68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 19 May 2022 20:32:08 +0200 Subject: [PATCH 42/80] Reload otp_devices to get default This appears to be more agressively cached by rails 7 now --- .../app/models/two_factor_authentication/device.rb | 2 +- .../spec/models/devices/default_device_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/two_factor_authentication/app/models/two_factor_authentication/device.rb b/modules/two_factor_authentication/app/models/two_factor_authentication/device.rb index 2c2e56290f..862b3e0da7 100644 --- a/modules/two_factor_authentication/app/models/two_factor_authentication/device.rb +++ b/modules/two_factor_authentication/app/models/two_factor_authentication/device.rb @@ -66,7 +66,7 @@ module TwoFactorAuthentication Device.transaction do Device.where(user_id: user_id).update_all(default: false) update_column(:default, true) - return true + true end end diff --git a/modules/two_factor_authentication/spec/models/devices/default_device_spec.rb b/modules/two_factor_authentication/spec/models/devices/default_device_spec.rb index 0433c4e57d..fc1ea59092 100644 --- a/modules/two_factor_authentication/spec/models/devices/default_device_spec.rb +++ b/modules/two_factor_authentication/spec/models/devices/default_device_spec.rb @@ -28,7 +28,7 @@ describe 'Default device', with_2fa_ee: true, type: :model do expect(user.otp_devices.get_default).to eq(other_otp) subject.make_default! - expect(user.otp_devices.get_default).to eq(subject) + expect(user.otp_devices.reload.get_default).to eq(subject) end end end From b26a7bdbfa196e20d0c9510e248d8fc37f43635f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 19 May 2022 20:36:04 +0200 Subject: [PATCH 43/80] Don't add to errors manually This no longer works in rails 7 --- app/models/custom_option.rb | 2 +- modules/pdf_export/app/models/export_card_configuration.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/custom_option.rb b/app/models/custom_option.rb index ec3bd982a7..157cec1491 100644 --- a/app/models/custom_option.rb +++ b/app/models/custom_option.rb @@ -49,7 +49,7 @@ class CustomOption < ApplicationRecord def assure_at_least_one_option return if CustomOption.where(custom_field_id: custom_field_id).where.not(id: id).count > 0 - errors[:base] << I18n.t(:'activerecord.errors.models.custom_field.at_least_one_custom_option') + errors.add(:base, I18n.t(:'activerecord.errors.models.custom_field.at_least_one_custom_option')) throw :abort end diff --git a/modules/pdf_export/app/models/export_card_configuration.rb b/modules/pdf_export/app/models/export_card_configuration.rb index fbb734b363..532587d789 100644 --- a/modules/pdf_export/app/models/export_card_configuration.rb +++ b/modules/pdf_export/app/models/export_card_configuration.rb @@ -69,11 +69,11 @@ class ExportCardConfiguration < ApplicationRecord def validate(record) begin if record.rows.nil? || !(YAML::load(record.rows)).is_a?(Hash) - record.errors[:rows] << I18n.t('validation_error_yaml_is_badly_formed') + record.errors.add(:rows, I18n.t('validation_error_yaml_is_badly_formed')) return false end rescue Psych::SyntaxError => e - record.errors[:rows] << I18n.t('validation_error_yaml_is_badly_formed') + record.errors.add(:rows, I18n.t('validation_error_yaml_is_badly_formed')) return false end @@ -92,7 +92,7 @@ class ExportCardConfiguration < ApplicationRecord end end rescue ArgumentError => e - record.errors[:rows] << "#{I18n.t('yaml_error')} #{e.message}" + record.errors.add(:rows, "#{I18n.t('yaml_error')} #{e.message}") end end end From 6b93d6e5fe45407d9be060e037844e65338ff183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 19 May 2022 20:44:12 +0200 Subject: [PATCH 44/80] Don't return from transaction block --- app/controllers/concerns/user_invitation.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/concerns/user_invitation.rb b/app/controllers/concerns/user_invitation.rb index bdd6823095..ef09241eb2 100644 --- a/app/controllers/concerns/user_invitation.rb +++ b/app/controllers/concerns/user_invitation.rb @@ -139,10 +139,10 @@ module UserInvitation token = Token::Invitation.create! user: user user.save! - return [user, token] + [user, token] + else + [user, nil] end end - - [user, nil] end end From 2c71a4ee6d532b889151ce469163b4e0bef72a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 19 May 2022 21:00:47 +0200 Subject: [PATCH 45/80] Better selector for inline create button --- app/cells/enumerations/table_cell.rb | 1 + modules/costs/spec/features/time_entry/activity_spec.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/cells/enumerations/table_cell.rb b/app/cells/enumerations/table_cell.rb index 0b5f4e57ef..14e8c58443 100644 --- a/app/cells/enumerations/table_cell.rb +++ b/app/cells/enumerations/table_cell.rb @@ -24,6 +24,7 @@ module Enumerations link_to new_enumeration_path(type: model.name), aria: { label: t(:label_enumeration_new) }, class: 'wp-inline-create--add-link', + data: { 'qa-selector': "create-enumeration-#{model.name.underscore.dasherize}" }, title: t(:label_enumeration_new) do op_icon('icon icon-add') end diff --git a/modules/costs/spec/features/time_entry/activity_spec.rb b/modules/costs/spec/features/time_entry/activity_spec.rb index d8d449249d..427b09bd1d 100644 --- a/modules/costs/spec/features/time_entry/activity_spec.rb +++ b/modules/costs/spec/features/time_entry/activity_spec.rb @@ -39,7 +39,7 @@ describe 'Time entry activity', type: :feature do it 'supports CRUD' do visit enumerations_path - page.all('.wp-inline-create--add-link[title="New enumeration value"]').first.click + page.find('[data-qa-selector="create-enumeration-time-entry-activity"]').click fill_in 'Name', with: 'A new activity' click_on('Create') From 3307099392d83a2751bb231b9efbecf8b36c56a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 19 May 2022 21:43:30 +0200 Subject: [PATCH 46/80] Use the rails 7 cache format --- config/application.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/application.rb b/config/application.rb index 9dddef095f..353a1d4ed7 100644 --- a/config/application.rb +++ b/config/application.rb @@ -185,6 +185,9 @@ module OpenProject config.log_level = OpenProject::Configuration['log_level'].to_sym + # Enable the Rails 7 cache format + config.active_support.cache_format_version = 7.0 + def self.root_url Setting.protocol + "://" + Setting.host_name end From d827a8ed7fcdd51492494dcd4546315abc2c5e31 Mon Sep 17 00:00:00 2001 From: OpenProject Actions CI Date: Fri, 20 May 2022 03:30:17 +0000 Subject: [PATCH 47/80] update locales from crowdin [ci skip] --- config/locales/crowdin/es.yml | 96 +++++++++---------- .../avatars/config/locales/crowdin/js-es.yml | 4 +- modules/bim/config/locales/crowdin/es.yml | 20 ++-- modules/budgets/config/locales/crowdin/es.yml | 10 +- modules/costs/config/locales/crowdin/es.yml | 6 +- .../documents/config/locales/crowdin/es.yml | 4 +- .../config/locales/crowdin/js-es.yml | 2 +- .../grids/config/locales/crowdin/js-es.yml | 4 +- modules/meeting/config/locales/crowdin/es.yml | 2 +- .../pdf_export/config/locales/crowdin/es.yml | 2 +- .../recaptcha/config/locales/crowdin/es.yml | 2 +- .../config/locales/crowdin/es.yml | 4 +- .../config/locales/crowdin/js-es.yml | 6 +- .../xls_export/config/locales/crowdin/es.yml | 2 +- 14 files changed, 82 insertions(+), 82 deletions(-) diff --git a/config/locales/crowdin/es.yml b/config/locales/crowdin/es.yml index 237b3a7321..c446778573 100644 --- a/config/locales/crowdin/es.yml +++ b/config/locales/crowdin/es.yml @@ -55,12 +55,12 @@ es: alternative-color: "Color de énfasis intenso, que suele usarse para el botón más importante de una pantalla." content-link-color: "Color de fuente de la mayoría de los enlaces." primary-color: "Color principal." - primary-color-dark: "Se suelen usar colores más oscuros para la acción de pasar el ratón por encima." + primary-color-dark: "Se suelen usar versions oscuras del color principal para la acción de pasar el ratón por encima." header-item-bg-hover-color: "Color de fondo de los elementos interactivos del encabezado cuando se mantiene el ratón sobre estos." header-item-font-color: "Color de fuente de los elementos interactivos del encabezado." header-item-font-hover-color: "Color de fuente de los elementos interactivos del encabezado cuando se mantiene el ratón sobre estos." header-border-bottom-color: "Línea fina debajo del encabezado. Deje este campo vacío si no quiere que se muestre ninguna línea." - main-menu-bg-color: "Color de fondo del menú izquierdo." + main-menu-bg-color: "Color de fondo del menú lateral izquierdo." theme_warning: Al cambiar el tema, se sobrescribirá su estilo personalizado y, como consecuencia, se perderá el diseño. ¿Seguro que quiere continuar? enterprise: upgrade_to_ee: "Actualizar a Enterprise Edition" @@ -74,9 +74,9 @@ es: book_now: 'Reservar ahora' get_quote: 'Solicitar presupuesto' buttons: - upgrade: "Actualice ahora" + upgrade: "Actualizar ahora" contact: "Contáctenos para una demostración" - enterprise_info_html: "es una fuerte iniciativaEnterprise." + enterprise_info_html: "es una función Enterprise." upgrade_info: "Actualice a un plan de pago para activarlo y empezar a usarlo en su equipo." journal_aggregation: explanation: @@ -157,7 +157,7 @@ es: label_no_color: 'Sin color' custom_actions: actions: - name: 'Comportamiento' + name: 'Acciones' add: 'Añadir Acción' assigned_to: executing_user_value: '(Asignar al usuario que ejecuta)' @@ -297,7 +297,7 @@ es: no_results_content_text: Crear un nuevo tipo edit: settings: "Configuración" - form_configuration: "Configuración del formato" + form_configuration: "Configuración del formulario" projects: "Proyectos" enabled_projects: "Proyectos habilitados" edit_query: "Editar tabla" @@ -325,7 +325,7 @@ es: could_not_be_saved: "Los siguientes paquetes de trabajo no pudieron guardarse:" none_could_be_saved: "Ninguno de los %{total} paquetes de trabajo pudo ser actualizado." x_out_of_y_could_be_saved: "%{failing} de %{total} paquetes de trabajo no pudieron ser actualizados mientras que %{success} pudieron." - selected_because_descendants: "Mientras que %{selected} paquetes de trabajo seleccionados, en total %{total} paquetes de trabajo son afectados que incluye descendientes." + selected_because_descendants: "Mientras que %{selected} paquetes de trabajo seleccionados, en total %{total} paquetes de trabajo son afectados incluyendo descendientes." descendant: "descendiente seleccionado" move: no_common_statuses_exists: "No hay ningún estado disponible para todos los paquetes de trabajo seleccionados. Su estado no se puede cambiar." @@ -343,7 +343,7 @@ es: priority: no_results_title_text: No hay prioridades disponibles por el momento. type: - no_results_title_text: Actualmente no hay tipos disponibles. + no_results_title_text: Actualmente no hay tipos de paquete disponibles. version: no_results_title_text: No hay versiones disponibles por el momento. label_invitation: Invitación @@ -366,7 +366,7 @@ es: other: "Escriba el nombre de usuario de %{name} para verificar la eliminación. Después del envío, se le pedirá que confirme la contraseña." self: "Escriba su nombre de usuario (%{name}) para verificar la eliminación. Después del envío, se le pedirá que confirme la contraseña." error_inactive_activation_by_mail: > - Su cuenta aún no está activada. Para activar su cuenta, haga clic en el enlace que fue enviado por correo electrónico. + Su cuenta aún no está activada. Para activar su cuenta, haga clic en el enlace que fue enviado a su correo electrónico. error_inactive_manual_activation: > Su cuenta aún no está activada. Por favor, espere a que el administrador active su cuenta. error_self_registration_disabled: > @@ -405,7 +405,7 @@ es: comment: commented: "Comentado" #an object that this comment belongs to custom_action: - actions: "Comportamiento" + actions: "Acciones" custom_field: default_value: "Valor predeterminado" editable: "Editable" @@ -644,7 +644,7 @@ es: read_ian: read_on_creation: 'no puede establecerse a verdadero en la creación de notificaciones.' mail_reminder_sent: - set_on_creation: 'no puede establecerse a verdadero en la creación de notificaciones.' + set_on_creation: 'no puede establecerse al valor verdadero en la creación de notificaciones.' reason: no_notification_reason: 'no puede estar en blanco, ya que se han elegido las notificaciones en la aplicación como un canal.' reason_mail_digest: @@ -698,10 +698,10 @@ es: attributes: to: error_not_found: "el paquete de trabajo en posición 'a' no se ha encontrado o no es visible" - error_readonly: "no se puede modificar el enlace de una relación existente 'a'" + error_readonly: "la relación existente no se puede modificar 'a' enlace es inmutable" from: error_not_found: "el paquete de trabajo en la posición 'de' no se ha encontrado o no es visible" - error_readonly: "no se puede modificar el enlace de una relación existente 'de'" + error_readonly: "la relación existente no se puede modificar 'de' enlace es inmutable" repository: not_available: "El proveedor SMC no está disponible" not_whitelisted: "no está permitido por la configuración." @@ -734,7 +734,7 @@ es: parent: cannot_be_milestone: "no puede ser un hito." cannot_be_self_assigned: "no se puede asignar a su propio usuario." - cannot_be_in_another_project: "No puede estar en otro proyecto." + cannot_be_in_another_project: "no puede estar en otro proyecto." not_a_valid_parent: "no es válido." start_date: violates_relationships: "sólo se puede establecer a %{soonest_start} o posterior para que no se violen las relaciones de los paquetes de trabajo." @@ -818,8 +818,8 @@ es: project: "Proyecto" query: "Consulta personalizada" role: - one: "Perfil" - other: "Perfiles" + one: "Rol" + other: "Roles" type: "Tipo" user: "Usuario" version: "Versión" @@ -879,7 +879,7 @@ es: project: "Proyecto" responsible: "Responsable" role: "Perfil" - roles: "Perfiles" + roles: "Roles" start_date: "Fecha de inicio" status: "Estado" subject: "Asunto" @@ -922,7 +922,7 @@ es: backup_pending: Ya hay una copia de seguridad pendiente. limit_reached: Solo puede ejecutar %{limit} copias de seguridad al día. button_add: "Añadir" - button_add_comment: "Agregar comentario" + button_add_comment: "Añadir comentario" button_add_member: Añadir miembro button_add_watcher: "Añadir observador" button_annotate: "Anotar" @@ -956,7 +956,7 @@ es: button_generate: "Generar" button_list: "Lista" button_lock: "Bloquear" - button_login: "Ingresar" + button_login: "Iniciar sesión" button_move: "Mover" button_move_and_follow: "Mover y seguir" button_print: "Imprimir" @@ -985,7 +985,7 @@ es: button_watch: "Controlar" button_manage_menu_entry: "Configurar el menú" button_add_menu_entry: "Agregar el elemento de menú" - button_configure_menu_entry: "Configurar el menú" + button_configure_menu_entry: "Configurar elemento del menú" button_delete_menu_entry: "Eliminar el elemento del menú" consent: checkbox_label: He notado y doy mi consentimiento a lo anterior. @@ -1051,7 +1051,7 @@ es: #Use the strftime parameters for formats. #When no format has been given, it uses default. #You can provide other formats here if you like! - default: "%m/%d/%Y" + default: "%d.%m.%Y" long: "%B %d, %Y" short: "%b %d" #Don't forget the nil at the beginning; there's no such thing as a 0th month @@ -1077,10 +1077,10 @@ es: datetime: distance_in_words: about_x_hours: - one: "cerca de 1 hora" - other: "cerca de %{count} horas" + one: "alrededor de 1 hora" + other: "alrededor de %{count} horas" about_x_months: - one: "alrededor de un més" + one: "alrededor de un mes" other: "alrededor de %{count} meses" about_x_years: one: "alrededor de un año" @@ -1099,8 +1099,8 @@ es: one: "mas de un año" other: "mas de %{count} años" x_days: - one: "1 dia" - other: "%{count} dias" + one: "1 día" + other: "%{count} días" x_minutes: one: "un minuto" other: "%{count} minutos" @@ -1109,7 +1109,7 @@ es: other: "%{count} meses" x_seconds: one: "un segundo" - other: "%{count} segundo" + other: "%{count} segundos" units: hour: one: "una hora" @@ -1199,7 +1199,7 @@ es: description_toc_toggle: "Mostrar/ocultar tabla de contenidos" description_wiki_subpages_reassign: "Elija nueva página principal" #Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) - direction: ltr + direction: iad (ltr) ee: upsale: form_configuration: @@ -1317,10 +1317,10 @@ es: blocks: community: "Comunidad de OpenProject" upsale: - title: "Actualizar hasta versión corporativa" + title: "Actualizar hasta versión Enterprise" more_info: "Más información" links: - upgrade_enterprise_edition: "Actualizar hasta versión corporativa" + upgrade_enterprise_edition: "Actualizar hasta versión Enterprise" postgres_migration: "Migrando su instalación a PostgreSQL" user_guides: "Guia de usuario" faq: "Preguntas Frecuentes" @@ -1423,7 +1423,7 @@ es: label_attachment_plural: "Archivos" label_attribute: "Atributo" label_attribute_plural: "Atributos" - label_auth_source: "Modos de autentificación" + label_auth_source: "Modo de autentificación" label_auth_source_new: "Nuevo modo de autenticaficación" label_auth_source_plural: "Modos de autentificación" label_authentication: "Autentificación" @@ -1441,7 +1441,7 @@ es: label_forums_locked: "Bloqueado" label_forum_new: "Foro nuevo" label_forum_plural: "Foros" - label_forum_sticky: "Adhesivo" + label_forum_sticky: "Fijo" label_boolean: "Booleano" label_branch: "Sucursal" label_browse: "Explorar" @@ -1496,7 +1496,7 @@ es: label_current_version: "Version actual" label_custom_field_add_no_type: "Agregue este campo a un tipo de paquete de trabajo" label_custom_field_new: "Nuevo campo personalizado" - label_custom_field_plural: "Campos Personalizados" + label_custom_field_plural: "Campos personalizados" label_custom_field_default_type: "Tipo vacío" label_custom_style: "Diseño" label_database_version: "Versión de PostgreSQL" @@ -1709,7 +1709,7 @@ es: label_path_encoding: "Ruta de codificación" label_pdf_with_descriptions: "PDF con las descripciones" label_per_page: "Por página" - label_people: "Participantes" + label_people: "Personas" label_permissions: "Permisos" label_permissions_report: "Informe de permisos" label_personalize_page: "Personalizar esta página" @@ -1952,7 +1952,7 @@ es: link_name: 'Nuevo paquete de trabajo' link_name_type: '%{type_name} nuevo' mail: - actions: 'Comportamiento' + actions: 'Acciones' digests: including_mention_singular: 'incluyendo una mención' including_mention_plural: 'incluyendo %{number_mentioned} menciones' @@ -2050,7 +2050,7 @@ es: more_actions: "Más funciones" noscript_description: "Necesita activar JavaScript para poder usar OpenProject!" noscript_heading: "JavaScript desactivado" - noscript_learn_more: "Saber más" + noscript_learn_more: "Más información" notice_accessibility_mode: El modo de accesibilidad puede ser habilitado a través de tu cuenta [settings](url). notice_account_activated: "Su cuenta ha sido activada. Ahora puede iniciar sesión." notice_account_already_activated: La Cuenta ya había sido activada. @@ -2153,7 +2153,7 @@ es: permission_comment_news: "Comentar noticias" permission_commit_access: "Permiso de escritura sobre el repositorio (commit)" permission_copy_projects: "Copiar proyectos" - permission_create_backup: "Create backup" + permission_create_backup: "Crear copia de seguridad" permission_delete_work_package_watchers: "Eliminar los observadores" permission_delete_work_packages: "Eliminar paquetes de trabajo" permission_delete_messages: "Eliminar mensajes" @@ -2246,7 +2246,7 @@ es: assigned_to_role: "Asignación de roles" member_of_group: "Asignación de grupo" assignee_or_group: "Grupo al que pertenece o al que está asignado" - subproject_id: "Including subproject" + subproject_id: "Incluyendo subproyecto" only_subproject_id: "Sólo subproyecto" name_or_identifier: "Nombre o identificador" repositories: @@ -2360,7 +2360,7 @@ es: setting_apiv3_cors_origins: "Orígenes permitidos de la API de CORS (uso compartido de recursos entre orígenes) V3" setting_apiv3_cors_origins_text_html: > Si CORS está habilitado, estos son los orígenes que tienen permiso para acceder a la API de OpenProject.
Consulte la documentación sobre el encabezado Origin para obtener información sobre cómo especificar los valores esperados. - setting_apiv3_max_page_size: "Tamaño máximo de página" + setting_apiv3_max_page_size: "Tamaño máximo de página API" setting_apiv3_max_page_instructions_html: > Establecer el tamaño máximo de página con el que responderá la API. No será posible realizar peticiones API que devuelvan más valores en una sola página.
Advertencia: Por favor, cambie este valor sólo si está seguro de por qué lo necesita. Un ajuste alto impactará significativamente el rendimiento, mientras que un valor inferior a las opciones por página causará errores en las vistas paginadas. setting_apiv3_docs: "Documentación" @@ -2629,9 +2629,9 @@ es: time: "%I:%M %p" pm: "pm" timeframe: - show: "Mostrar calendario" + show: "Mostrar marco temporal" end: "a" - start: "De" + start: "de" timelines: admin_menu: color: "Color" @@ -2644,12 +2644,12 @@ es: current_planning: "Planificación actual" dates: "Fechas" dates_are_calculated_based_on_sub_elements: "Las fechas se calculan basándose en sub elementos." - delete_all: "Borrar todo" + delete_all: "Borrar todos" delete_thing: "Borrar" duration: "Duración" duration_days: - one: "1 dia" - other: "%{count} dias" + one: "1 día" + other: "%{count} días" edit_color: "Editar colores" edit_thing: "Editar" edit_timeline: "Edición del informe de tiempos %{timeline}" @@ -2719,7 +2719,7 @@ es: status: "Mostrar estado" project_time_filter: "Proyectos con paquete de trabajo de un tipo determinado en un plazo" project_time_filter_timeframe: "Marco de tiempo" - project_time_filter_historical_from: "De" + project_time_filter_historical_from: "de" project_time_filter_historical_to: "a" project_time_filter_historical: "%{start_label} %{startdate} %{end_label} %{enddate}" project_time_filter_relative: "%{start_label} %{startspan}%{startspanunit} hace, %{end_label} %{endspan}%{endspanunit} desde ahora" @@ -2768,7 +2768,7 @@ es: ¿Esta seguro que desea eliminar el siguiente informe? Los informes de estados anteriores se eliminaran, también. start: "Inicio" timeline: "Informe de línea de tiempo" - timelines: "Informes de cronograma" + timelines: "Informe de línea de tiempo" settings: "Líneas de tiempo" vertical_work_package: "Paquetes de trabajo vertical" you_are_viewing_the_selected_timeline: "Usted está viendo el informe de línea de tiempo seleccionado" @@ -2809,7 +2809,7 @@ es: blocked: "bloqueado temporalmente" blocked_num_failed_logins: one: "bloqueado temporalmente (un intento fallido de inicio de sesión)" - other: "bloqueada temporalmente (%{count} intentos de ingreso fallidos)" + other: "bloqueada temporalmente (%{count} intentos fallidos de inicio de sesión)" confirm_status_change: "Va a cambiar el estado de “%{name}”. ¿Está seguro de que quiere continuar?" deleted: "Usuarios Eliminados" error_status_change_failed: "Cambiar la condición de usuario falló debido a los errores siguientes: %{errors}" @@ -2955,7 +2955,7 @@ es: expired: "El token de acceso ha expirado" unknown: "El token de acceso es inválido" revoke: - unauthorized: "No estás autorizado a revocar este bono." + unauthorized: "No estás autorizado a revocar este token." forbidden_token: missing_scope: 'El acceso a este recurso requiere competencia "%{oauth_scopes}".' unsupported_browser: diff --git a/modules/avatars/config/locales/crowdin/js-es.yml b/modules/avatars/config/locales/crowdin/js-es.yml index 29430789c0..48867e9424 100644 --- a/modules/avatars/config/locales/crowdin/js-es.yml +++ b/modules/avatars/config/locales/crowdin/js-es.yml @@ -10,6 +10,6 @@ es: Cargue su propio avatar personalizado de 128 × 128 píxeles. Si se agregan archivos más grandes, se cambiará el tamaño y se recortará para que coincida. Después de seleccionar una imagen, se mostrará una vista previa del avatar antes de cargarlo. error_image_too_large: "La imagen es demasiado grande." - wrong_file_format: "Los formatos permitidos son JPG, PNG y GIF" - empty_file_error: "Cargue un archivo de imagen válido (JPG, PNG o GIF)" + wrong_file_format: "Los formatos permitidos son jpg, png y gif" + empty_file_error: "Cargue un formato de imagen válido (jpg, png o gif)" diff --git a/modules/bim/config/locales/crowdin/es.yml b/modules/bim/config/locales/crowdin/es.yml index b84d5c92b9..ad67cdaf8b 100644 --- a/modules/bim/config/locales/crowdin/es.yml +++ b/modules/bim/config/locales/crowdin/es.yml @@ -78,7 +78,7 @@ es: attributes: bim/ifc_models/ifc_model: ifc_attachment: "Archivo IFC" - is_default: "Modelo predeterminado" + is_default: "Modelo por defecto" attachments: "Archivo IFC" errors: models: @@ -88,17 +88,17 @@ es: ifc_attachment_missing: "No se adjuntó ningún archivo IFC." invalid_ifc_file: "El archivo proporcionado no es un archivo IFC válido." bim/bcf/viewpoint: - bitmaps_not_writable: "«bitmaps» no permite la escritura, ya que aún no se ha implementado." - index_not_integer: "«index» no es un entero." - invalid_clipping_planes: "«clipping_planes» no es válido." - invalid_components: "«components» no es válido." - invalid_lines: "«lines» no es válido." - invalid_orthogonal_camera: "«orthogonal_camera» no es válido." - invalid_perspective_camera: "«perspective_camera» no es válido." + bitmaps_not_writable: "los bitmaps no permiten la escritura, ya que aún no se han implementado." + index_not_integer: "index no es un entero." + invalid_clipping_planes: "clipping_planes no es válido." + invalid_components: "components no es válido." + invalid_lines: "lines no es válido." + invalid_orthogonal_camera: "orthogonal_camera no es válido." + invalid_perspective_camera: "perspective_camera no es válido." mismatching_guid: "El GUID de json_viewpoint no coincide con el GUID del modelo." no_json: "La estructura del código JSON no es correcta." - snapshot_type_unsupported: "«snapshot_type» tiene que ser «png» o «jpg»." - snapshot_data_blank: "«snapshot_data» tiene que especificarse." + snapshot_type_unsupported: "snapshot_type tiene que ser png o jpg." + snapshot_data_blank: "snapshot_data tiene que ser especificada." unsupported_key: "Se ha incluido una propiedad JSON no admitida." bim/bcf/issue: uuid_already_taken: "No se puede importar el defecto de BCF porque ya existe otro con el mismo GUID ¿Podría ser \nque este defecto BCF ya fue importado a un proyecto diferente?" diff --git a/modules/budgets/config/locales/crowdin/es.yml b/modules/budgets/config/locales/crowdin/es.yml index 3d613c52df..0b2bfbbc12 100644 --- a/modules/budgets/config/locales/crowdin/es.yml +++ b/modules/budgets/config/locales/crowdin/es.yml @@ -26,14 +26,14 @@ es: author: "Autor" available: "Disponible" budget: "Planeado" - budget_ratio: "Gastado (cociente)" + budget_ratio: "Gastado (proporción)" description: "Descripción" - spent: "Pasado" + spent: "Gastado" status: "Estado" subject: "Asunto" type: "Tipo de costo" - labor_budget: "Costos laborales previstos" - material_budget: "Costos unitarios previstos" + labor_budget: "Costos laborales planeados" + material_budget: "Costos unitarios planeados" work_package: budget_subject: "Título de presupuesto" models: @@ -63,7 +63,7 @@ es: label_example_placeholder: 'Por ejemplo, %{decimal}' label_view_all_budgets: "Ver todos los presupuestos" label_yes: "Sí" - notice_budget_conflict: "Paquetes de trabajo deben de estar en un mismo proyecto." + notice_budget_conflict: "Los paquetes de trabajo deben de estar en un mismo proyecto." notice_no_budgets_available: "No hay presupuestos disponibles." permission_edit_budgets: "Editar presupuestos" permission_view_budgets: "Ver los presupuestos" diff --git a/modules/costs/config/locales/crowdin/es.yml b/modules/costs/config/locales/crowdin/es.yml index 1e832c7d6d..d3ae95472e 100644 --- a/modules/costs/config/locales/crowdin/es.yml +++ b/modules/costs/config/locales/crowdin/es.yml @@ -45,7 +45,7 @@ es: cost_type: one: "Tipo de costo" other: "Tipos de costos" - rate: "Tasa" + rate: "Proporción" errors: models: work_package: @@ -105,13 +105,13 @@ es: label_no: "No" label_option_plural: "Opciones" label_overall_costs: "Costos totales" - label_rate: "Tasa" + label_rate: "Proporción" label_rate_plural: "Tasas" label_status_finished: "Acabado" label_units: "Unidades de costo" label_user: "Usuario" label_until: "hasta" - label_valid_from: "Válida desde" + label_valid_from: "Válido desde" label_yes: "Sí" notice_something_wrong: "Algo salió mal. Por favor intentelo nuevamente." notice_successful_restore: "Restauración exitosa." diff --git a/modules/documents/config/locales/crowdin/es.yml b/modules/documents/config/locales/crowdin/es.yml index 336197b092..90c836fd05 100644 --- a/modules/documents/config/locales/crowdin/es.yml +++ b/modules/documents/config/locales/crowdin/es.yml @@ -22,9 +22,9 @@ es: activerecord: models: - document: "Dokument" + document: "Document" default_doc_category_tech: "Documentación técnica" - default_doc_category_user: "Documentacion para el usuario" + default_doc_category_user: "Documentación para el usuario" enumeration_doc_categories: "Categorías de documentos" enumeration: document_category: diff --git a/modules/github_integration/config/locales/crowdin/js-es.yml b/modules/github_integration/config/locales/crowdin/js-es.yml index ced0592796..12d318d0cd 100644 --- a/modules/github_integration/config/locales/crowdin/js-es.yml +++ b/modules/github_integration/config/locales/crowdin/js-es.yml @@ -38,4 +38,4 @@ es: copy_error: '❌ Error al copiar' tab_prs: empty: 'Aún no hay solicitudes de incorporación de cambios vinculadas. Vincule una solicitud de incorporación de cambios existente con el código OP#%{wp_id} en la descripción, o bien cree una nueva solicitud de incorporación de cambios.' - github_actions: Comportamiento + github_actions: Acciones diff --git a/modules/grids/config/locales/crowdin/js-es.yml b/modules/grids/config/locales/crowdin/js-es.yml index 44def43836..45b0b96a05 100644 --- a/modules/grids/config/locales/crowdin/js-es.yml +++ b/modules/grids/config/locales/crowdin/js-es.yml @@ -46,11 +46,11 @@ es: displayed_days: 'Días mostrados en el widget:' time_entries_list: title: 'Tiempo gastado (últimos 7 días)' - no_results: 'Sin entradas temporales en los últimos 7 días.' + no_results: 'Sin registros temporales en los últimos 7 días.' work_packages_accountable: title: "Paquetes de trabajo de los que soy responsable" work_packages_assigned: - title: 'Paquetes de trabajo asignados para mí' + title: 'Paquetes de trabajo asignados a mí' work_packages_created: title: 'Paquetes de trabajo creados por mí' work_packages_watched: diff --git a/modules/meeting/config/locales/crowdin/es.yml b/modules/meeting/config/locales/crowdin/es.yml index 2f6c7bbb1f..4ab1e93662 100644 --- a/modules/meeting/config/locales/crowdin/es.yml +++ b/modules/meeting/config/locales/crowdin/es.yml @@ -86,5 +86,5 @@ es: text_meeting_agenda_open_are_you_sure: "Se sobrescribirán todos los cambios en las actas. ¿Quiere continuar?" text_meeting_minutes_for_meeting: 'minutos para la reunión "%{meeting}"' text_review_meeting_agenda: "%{author} ha puesto el %{link} para revisión." - text_review_meeting_minutes: "%{author} ha puesto el %{link} para revisión." + text_review_meeting_minutes: "%{author} ha definido el %{link} para revisión." text_notificiation_invited: "El correo electrónico contiene una entrada ics para la reunión siguiente:" diff --git a/modules/pdf_export/config/locales/crowdin/es.yml b/modules/pdf_export/config/locales/crowdin/es.yml index 148533eb10..b35a90609d 100644 --- a/modules/pdf_export/config/locales/crowdin/es.yml +++ b/modules/pdf_export/config/locales/crowdin/es.yml @@ -47,5 +47,5 @@ es: export_card_configuration: rows: "Filas" per_page: "Por página" - page_size: "Tamaño de la página" + page_size: "Tamaño de página" orientation: "Orientación" diff --git a/modules/recaptcha/config/locales/crowdin/es.yml b/modules/recaptcha/config/locales/crowdin/es.yml index ccd584326b..ab3bbba245 100644 --- a/modules/recaptcha/config/locales/crowdin/es.yml +++ b/modules/recaptcha/config/locales/crowdin/es.yml @@ -8,7 +8,7 @@ es: settings: website_key: 'Clave del sitio web' website_key_text: 'Especifique la clave del sitio web que ha creado en la consola de administración de reCAPTCHA para este dominio.' - secret_key: 'Llave secreta' + secret_key: 'Clave secreta' secret_key_text: 'Especifique la clave secreta que ha creado en la consola de administración de reCAPTCHA.' type: 'Usar reCAPTCHA' type_disabled: 'Deshabilitar reCAPTCHA' diff --git a/modules/team_planner/config/locales/crowdin/es.yml b/modules/team_planner/config/locales/crowdin/es.yml index f0eae98852..fafad9bd83 100644 --- a/modules/team_planner/config/locales/crowdin/es.yml +++ b/modules/team_planner/config/locales/crowdin/es.yml @@ -1,13 +1,13 @@ #English strings go here es: permission_view_team_planner: "Ver planificador del equipo" - permission_manage_team_planner: "Gestionar plan de equipo" + permission_manage_team_planner: "Gestionar planificador del equipo" project_module_team_planner_view: "Planificador de equipo" team_planner: label_team_planner: "Planificador de equipo" label_create_new_team_planner: "Crear planificador de equipo" label_team_planner_plural: "Planificadores de equipos" - label_assignees: "Asignados" + label_assignees: "Asignado a" upsale: title: "Planificador de equipo" description: "El planificador de equipo muestra un resumen completo de las tareas asignadas a los miembros del equipo, semana a semana. Puede mover, ampliar y reducir visualmente los paquetes de trabajo, e incluso arrastrarlos de un usuario a otro para organizar las cargas de trabajo. ¡Hasta puede crear nuevos paquetes de trabajo o añadir existentes desde el planificador de equipo!" diff --git a/modules/team_planner/config/locales/crowdin/js-es.yml b/modules/team_planner/config/locales/crowdin/js-es.yml index cc6cfcd6ef..cee48219b0 100644 --- a/modules/team_planner/config/locales/crowdin/js-es.yml +++ b/modules/team_planner/config/locales/crowdin/js-es.yml @@ -4,10 +4,10 @@ es: team_planner: add_existing: 'Añadir existente' title: 'Planificador de equipos' - unsaved_title: 'Planificador de equipo anonimo' - no_data: 'Añadir asignados para configurar el planificador de su equipo.' + unsaved_title: 'Planificador de equipo sin nombre' + no_data: 'Añadir asignados para configurar su planificador de equipo.' add_assignee: 'Añadir asignado' - remove_assignee: 'Quitar asignado' + remove_assignee: 'Eliminar asignado' two_weeks: '2 semanas' one_week: '1 semana' today: 'Hoy' diff --git a/modules/xls_export/config/locales/crowdin/es.yml b/modules/xls_export/config/locales/crowdin/es.yml index 221146eadc..02b9c600fc 100644 --- a/modules/xls_export/config/locales/crowdin/es.yml +++ b/modules/xls_export/config/locales/crowdin/es.yml @@ -1,7 +1,7 @@ es: export_to_excel: "Exportar a XLS" print_with_description: "Vista previa de impresión con descripción" - sentence_separator_or: "o bien" + sentence_separator_or: "o" different_formats: Diferentes formatos export: format: From 8da459140e8503f7b9628136e0403cd18aa4228d Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Thu, 19 May 2022 18:12:44 +0200 Subject: [PATCH 48/80] refactor: expand options argument of allowed_to calls Clean Authorization::UserAllowedService methods to remove global and options parameters when they are not used use allowed_to_globally? where possible --- .rubocop.yml | 2 +- .../work_packages/create_contract.rb | 2 +- app/helpers/projects_helper.rb | 2 +- .../work_packages/filter/relatable_filter.rb | 2 +- app/models/user.rb | 12 +++--- .../authorization/user_allowed_service.rb | 40 ++++++++++--------- .../homescreen/blocks/_projects.html.erb | 2 +- app/views/projects/index.html.erb | 2 +- config/initializers/menus.rb | 4 +- lib/api/v3/root_representer.rb | 4 +- .../lib/acts/journalized/permissions.rb | 3 +- .../controllers/hourly_rates_controller.rb | 2 +- .../controllers/cost_reports_controller.rb | 39 +++++++----------- .../lib/open_project/reporting/engine.rb | 8 ++-- .../spec/features/top_menu_item_spec.rb | 2 +- .../_view_cost_report_toolbar.html.erb | 4 +- spec/lib/api/v3/root_representer_spec.rb | 6 +-- 17 files changed, 65 insertions(+), 71 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 6abcb51bc2..5ddc14858d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -3,7 +3,7 @@ require: - rubocop-rspec AllCops: - TargetRubyVersion: 3.0 + TargetRubyVersion: 3.1 # Enable any new cops in new versions by default NewCops: enable Exclude: diff --git a/app/contracts/work_packages/create_contract.rb b/app/contracts/work_packages/create_contract.rb index ce675a6476..528fac1ab5 100644 --- a/app/contracts/work_packages/create_contract.rb +++ b/app/contracts/work_packages/create_contract.rb @@ -43,7 +43,7 @@ module WorkPackages def user_allowed_to_add if (model.project && !@user.allowed_to?(:add_work_packages, model.project)) || - !@user.allowed_to?(:add_work_packages, nil, global: true) + !@user.allowed_to_globally?(:add_work_packages) errors.add :base, :error_unauthorized end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index f1cbb1fb83..4a81334888 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -57,7 +57,7 @@ module ProjectsHelper end def no_projects_result_box_params - if User.current.allowed_to?(:add_project, nil, global: true) + if User.current.allowed_to_globally?(:add_project) { action_url: new_project_path, display_action: true } else {} diff --git a/app/models/queries/work_packages/filter/relatable_filter.rb b/app/models/queries/work_packages/filter/relatable_filter.rb index 0e1ceed63b..3c9a87a6b4 100644 --- a/app/models/queries/work_packages/filter/relatable_filter.rb +++ b/app/models/queries/work_packages/filter/relatable_filter.rb @@ -30,7 +30,7 @@ class Queries::WorkPackages::Filter::RelatableFilter < Queries::WorkPackages::Fi include Queries::WorkPackages::Filter::FilterForWpMixin def available? - User.current.allowed_to?(:manage_work_package_relations, nil, global: true) + User.current.allowed_to_globally?(:manage_work_package_relations) end def type diff --git a/app/models/user.rb b/app/models/user.rb index d12c97a344..0f58cb40fc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -518,16 +518,16 @@ class User < Principal Authorization.users(action, project).where.not(members: { id: nil }) end - def allowed_to?(action, context, options = {}) - authorization_service.call(action, context, options).result + def allowed_to?(action, context, global: false) + authorization_service.call(action, context, global:).result end - def allowed_to_in_project?(action, project, options = {}) - authorization_service.call(action, project, options).result + def allowed_to_in_project?(action, project) + allowed_to?(action, project) end - def allowed_to_globally?(action, options = {}) - authorization_service.call(action, nil, options.merge(global: true)).result + def allowed_to_globally?(action) + allowed_to?(action, nil, global: true) end delegate :preload_projects_allowed_to, to: :authorization_service diff --git a/app/services/authorization/user_allowed_service.rb b/app/services/authorization/user_allowed_service.rb index a390aa10ef..7480028051 100644 --- a/app/services/authorization/user_allowed_service.rb +++ b/app/services/authorization/user_allowed_service.rb @@ -41,12 +41,12 @@ class Authorization::UserAllowedService # Context can be: # * a project : returns true if user is allowed to do the specified action on this project # * a group of projects : returns true if user is allowed on every project - # * nil with options[:global] set : check if user has at least one role allowed for this action, + # * nil with +global+ set to +true+ : check if user has at least one role allowed for this action, # or falls back to Non Member / Anonymous permissions depending if the user is logged - def call(action, context, options = {}) - if supported_context?(context, options) + def call(action, context, global: false) + if supported_context?(context, global:) ServiceResult.new(success: true, - result: allowed_to?(action, context, options)) + result: allowed_to?(action, context, global:)) else ServiceResult.new(success: false, result: false) @@ -61,25 +61,21 @@ class Authorization::UserAllowedService attr_accessor :project_role_cache - def allowed_to?(action, context, options = {}) + def allowed_to?(action, context, global: false) action = normalize_action(action) - if context.nil? && options[:global] - allowed_to_globally?(action, options) + if context.nil? && global + allowed_to_globally?(action) elsif context.is_a? Project - allowed_to_in_project?(action, context, options) + allowed_to_in_project?(action, context) elsif context.respond_to?(:to_a) - context = context.to_a - # Authorize if user is authorized on every element of the array - context.present? && context.all? do |project| - allowed_to?(action, project, options) - end + allowed_to_in_all_projects?(action, context) else false end end - def allowed_to_in_project?(action, project, _options = {}) + def allowed_to_in_project?(action, project) if project_authorization_cache.cached?(action) return project_authorization_cache.allowed?(action, project) end @@ -97,9 +93,17 @@ class Authorization::UserAllowedService has_authorized_role?(action, project) end + def allowed_to_in_all_projects?(action, projects) + projects = projects.to_a + # Authorize if user is authorized on every element of the array + projects.present? && projects.all? do |project| + allowed_to?(action, project) + end + end + # Is the user allowed to do the specified action on any project? - # See allowed_to? for the actions and valid options. - def allowed_to_globally?(action, _options = {}) + # See allowed_to? for the action parameter description. + def allowed_to_globally?(action) # Inactive users are never authorized return false unless authorizable_user? # Admin users are always authorized @@ -142,8 +146,8 @@ class Authorization::UserAllowedService action end - def supported_context?(context, options) - (context.nil? && options[:global]) || + def supported_context?(context, global:) + (context.nil? && global) || context.is_a?(Project) || (!context.nil? && context.respond_to?(:to_a)) end diff --git a/app/views/homescreen/blocks/_projects.html.erb b/app/views/homescreen/blocks/_projects.html.erb index 2b1a252a12..66d1e2cf4c 100644 --- a/app/views/homescreen/blocks/_projects.html.erb +++ b/app/views/homescreen/blocks/_projects.html.erb @@ -17,7 +17,7 @@ <% end %>
- <% if User.current.allowed_to?(:add_project, nil, global: true) %> + <% if User.current.allowed_to_globally?(:add_project) %> <%= link_to new_project_path, { class: 'button -alt-highlight', aria: {label: t(:label_project_new)}, diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 77a0eabbe0..a472ccec78 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -33,7 +33,7 @@ See COPYRIGHT and LICENSE files for more details. <% html_title(t(:label_project_plural)) -%> <%= toolbar title: t(:label_project_plural), html: { class: '-with-dropdown' } do %> - <% if User.current.allowed_to?(:add_project, nil, global: true) %> + <% if User.current.allowed_to_globally?(:add_project) %>
  • <%= link_to new_project_path, { class: 'button -alt-highlight', diff --git a/config/initializers/menus.rb b/config/initializers/menus.rb index bd6d14d989..627fa034cc 100644 --- a/config/initializers/menus.rb +++ b/config/initializers/menus.rb @@ -44,7 +44,7 @@ Redmine::MenuManager.map :top_menu do |menu| caption: I18n.t('label_work_package_plural'), if: Proc.new { (User.current.logged? || !Setting.login_required?) && - User.current.allowed_to?(:view_work_packages, nil, global: true) + User.current.allowed_to_globally?(:view_work_packages) } menu.push :news, { controller: '/news', project_id: nil, action: 'index' }, @@ -52,7 +52,7 @@ Redmine::MenuManager.map :top_menu do |menu| caption: I18n.t('label_news_plural'), if: Proc.new { (User.current.logged? || !Setting.login_required?) && - User.current.allowed_to?(:view_news, nil, global: true) + User.current.allowed_to_globally?(:view_news) } menu.push :help, OpenProject::Static::Links.help_link, diff --git a/lib/api/v3/root_representer.rb b/lib/api/v3/root_representer.rb index b0c6a8a89f..4341170e05 100644 --- a/lib/api/v3/root_representer.rb +++ b/lib/api/v3/root_representer.rb @@ -44,8 +44,8 @@ module API end link :memberships do - next unless current_user.allowed_to?(:view_members, nil, global: true) || - current_user.allowed_to?(:manage_members, nil, global: true) + next unless current_user.allowed_to_globally?(:view_members) || + current_user.allowed_to_globally?(:manage_members) { href: api_v3_paths.memberships diff --git a/lib/plugins/acts_as_journalized/lib/acts/journalized/permissions.rb b/lib/plugins/acts_as_journalized/lib/acts/journalized/permissions.rb index 5318b6b31a..95132227e6 100644 --- a/lib/plugins/acts_as_journalized/lib/acts/journalized/permissions.rb +++ b/lib/plugins/acts_as_journalized/lib/acts/journalized/permissions.rb @@ -56,8 +56,7 @@ module Acts::Journalized editable_by?(user) else p = @project || (project if respond_to? :project) - options = { global: p.present? } - user.allowed_to? journable_edit_permission, p, options + user.allowed_to? journable_edit_permission, p, global: p.present? end editable && journal.user_id == user.id diff --git a/modules/costs/app/controllers/hourly_rates_controller.rb b/modules/costs/app/controllers/hourly_rates_controller.rb index 3e0040ffc1..3ab10b2c24 100644 --- a/modules/costs/app/controllers/hourly_rates_controller.rb +++ b/modules/costs/app/controllers/hourly_rates_controller.rb @@ -44,7 +44,7 @@ class HourlyRatesController < ApplicationController # TODO: this should be an index def show if @project - return deny_access unless User.current.allowed_to?(:view_hourly_rates, @project, for: @user) + return deny_access unless User.current.allowed_to?(:view_hourly_rates, @project) @rates = HourlyRate.where(user_id: @user, project_id: @project) .order("#{HourlyRate.table_name}.valid_from desc") diff --git a/modules/reporting/app/controllers/cost_reports_controller.rb b/modules/reporting/app/controllers/cost_reports_controller.rb index b8f9777630..d5ae5d38b2 100644 --- a/modules/reporting/app/controllers/cost_reports_controller.rb +++ b/modules/reporting/app/controllers/cost_reports_controller.rb @@ -359,31 +359,22 @@ class CostReportsController < ApplicationController # If report does not belong to a project, it is ok to look for the # permission in any project. Otherwise, the user should have the permission # in this project. - options = if report.project.present? - {} - else - { global: true } - end - - case action - when :create - user.allowed_to?(:save_cost_reports, @project, options) or - user.allowed_to?(:save_private_cost_reports, @project, options) - - when :save, :destroy, :rename - if report.is_public? - user.allowed_to?(:save_cost_reports, @project, options) - else - user.allowed_to?(:save_cost_reports, @project, options) or - user.allowed_to?(:save_private_cost_reports, @project, options) + global = report.project.nil? + + permissions = + case action + when :create + %i[save_cost_reports save_private_cost_reports] + when :save, :destroy, :rename + if report.is_public? + %i[save_cost_reports] + else + %i[save_cost_reports save_private_cost_reports] + end + when :save_as_public + %i[save_cost_reports] end - - when :save_as_public - user.allowed_to?(:save_cost_reports, @project, options) - - else - false - end + Array(permissions).any? { |permission| user.allowed_to?(permission, @project, global:) } end def display_report_list diff --git a/modules/reporting/lib/open_project/reporting/engine.rb b/modules/reporting/lib/open_project/reporting/engine.rb index 78e7bd14af..572524dc8c 100644 --- a/modules/reporting/lib/open_project/reporting/engine.rb +++ b/modules/reporting/lib/open_project/reporting/engine.rb @@ -62,10 +62,10 @@ module OpenProject::Reporting if: Proc.new { (User.current.logged? || !Setting.login_required?) && ( - User.current.allowed_to?(:view_time_entries, nil, global: true) || - User.current.allowed_to?(:view_own_time_entries, nil, global: true) || - User.current.allowed_to?(:view_cost_entries, nil, global: true) || - User.current.allowed_to?(:view_own_cost_entries, nil, global: true) + User.current.allowed_to_globally?(:view_time_entries) || + User.current.allowed_to_globally?(:view_own_time_entries) || + User.current.allowed_to_globally?(:view_cost_entries) || + User.current.allowed_to_globally?(:view_own_cost_entries) ) } diff --git a/modules/reporting/spec/features/top_menu_item_spec.rb b/modules/reporting/spec/features/top_menu_item_spec.rb index 5ce55365f7..d071579328 100644 --- a/modules/reporting/spec/features/top_menu_item_spec.rb +++ b/modules/reporting/spec/features/top_menu_item_spec.rb @@ -65,7 +65,7 @@ feature 'Top menu items', js: true do create(:non_member) if ex.metadata.key?(:allowed_to) - allow(user).to receive(:allowed_to?).and_return(ex.metadata[:allowed_to]) + allow(user).to receive(:allowed_to_globally?).and_return(ex.metadata[:allowed_to]) end visit root_path diff --git a/modules/xls_export/app/views/hooks/xls_report/_view_cost_report_toolbar.html.erb b/modules/xls_export/app/views/hooks/xls_report/_view_cost_report_toolbar.html.erb index 25419b6ba6..518faed2eb 100644 --- a/modules/xls_export/app/views/hooks/xls_report/_view_cost_report_toolbar.html.erb +++ b/modules/xls_export/app/views/hooks/xls_report/_view_cost_report_toolbar.html.erb @@ -1,10 +1,10 @@ -<% if User.current.allowed_to? :export_work_packages, @project, :global => @project.nil? %> +<% if User.current.allowed_to? :export_work_packages, @project, global: @project.nil? %>
  • <%= link_to(t(:export_to_excel), { controller: "cost_reports" , action: :index, format: 'xls', project_id: @project }, - :class => "button icon-export-xls-descr") %> + class: "button icon-export-xls-descr") %>
  • <% end %> diff --git a/spec/lib/api/v3/root_representer_spec.rb b/spec/lib/api/v3/root_representer_spec.rb index 666fde0976..95053f9ee5 100644 --- a/spec/lib/api/v3/root_representer_spec.rb +++ b/spec/lib/api/v3/root_representer_spec.rb @@ -39,9 +39,9 @@ describe ::API::V3::RootRepresenter do before do allow(user) - .to receive(:allowed_to?) do |action, _project, options| - permissions.include?(action) && options[:global] = true - end + .to receive(:allowed_to_globally?) do |action| + permissions.include?(action) + end end context 'generation' do From dd427e68cb693df0fab80d3d032a5cecab97818a Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Fri, 20 May 2022 10:22:58 +0200 Subject: [PATCH 49/80] remove useless Authorization::UserAllowedService instantiations --- app/cells/placeholder_users/table_cell.rb | 4 ---- .../placeholder_users/delete_contract.rb | 15 +++++++-------- app/helpers/placeholder_users_helper.rb | 3 +-- app/models/user.rb | 4 ++-- .../spec/controllers/calendar_controller_spec.rb | 5 +++-- 5 files changed, 13 insertions(+), 18 deletions(-) diff --git a/app/cells/placeholder_users/table_cell.rb b/app/cells/placeholder_users/table_cell.rb index 2e106831ed..6b4b5532cc 100644 --- a/app/cells/placeholder_users/table_cell.rb +++ b/app/cells/placeholder_users/table_cell.rb @@ -52,9 +52,5 @@ module PlaceholderUsers def desc_by_default [:created_at] end - - def user_allowed_service - @user_allowed_service ||= Authorization::UserAllowedService.new(options[:current_user]) - end end end diff --git a/app/contracts/placeholder_users/delete_contract.rb b/app/contracts/placeholder_users/delete_contract.rb index e4ddc12be9..f4ea860d00 100644 --- a/app/contracts/placeholder_users/delete_contract.rb +++ b/app/contracts/placeholder_users/delete_contract.rb @@ -36,19 +36,18 @@ module PlaceholderUsers # Checks if a given placeholder user may be deleted by a user. # # @param actor [User] User who wants to delete the given placeholder user. - def self.deletion_allowed?(placeholder_user, - actor, - user_allowed_service = Authorization::UserAllowedService.new(actor)) + def self.deletion_allowed?(placeholder_user, actor) actor.allowed_to_globally?(:manage_placeholder_user) && - affected_projects_managed_by_actor?(placeholder_user, user_allowed_service) + affected_projects_managed_by_actor?(placeholder_user, actor) end - protected - - def self.affected_projects_managed_by_actor?(placeholder_user, user_allowed_service) + def self.affected_projects_managed_by_actor?(placeholder_user, actor) placeholder_user.projects.active.empty? || - user_allowed_service.call(:manage_members, placeholder_user.projects.active).result + actor.allowed_to?(:manage_members, placeholder_user.projects.active) end + private_class_method :affected_projects_managed_by_actor? + + protected def deletion_allowed? self.class.deletion_allowed?(model, user) diff --git a/app/helpers/placeholder_users_helper.rb b/app/helpers/placeholder_users_helper.rb index c493ca4b70..a10da673b1 100644 --- a/app/helpers/placeholder_users_helper.rb +++ b/app/helpers/placeholder_users_helper.rb @@ -31,7 +31,6 @@ module PlaceholderUsersHelper # Determine whether the given actor can delete the placeholder user def can_delete_placeholder_user?(placeholder, actor = User.current) PlaceholderUsers::DeleteContract.deletion_allowed? placeholder, - actor, - Authorization::UserAllowedService.new(actor) + actor end end diff --git a/app/models/user.rb b/app/models/user.rb index 0f58cb40fc..5772aa38e4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -164,8 +164,8 @@ class User < Principal def reload(*args) @name = nil @projects_by_role = nil - @authorization_service = ::Authorization::UserAllowedService.new(self) - @project_role_cache = ::Users::ProjectRoleCache.new(self) + @authorization_service = nil + @project_role_cache = nil super end diff --git a/modules/calendar/spec/controllers/calendar_controller_spec.rb b/modules/calendar/spec/controllers/calendar_controller_spec.rb index 21ac77c268..4361aa2352 100644 --- a/modules/calendar/spec/controllers/calendar_controller_spec.rb +++ b/modules/calendar/spec/controllers/calendar_controller_spec.rb @@ -41,11 +41,12 @@ describe ::Calendar::CalendarsController, type: :controller do let(:user) do build_stubbed(:user).tap do |user| allow(user) - .to receive(:allowed_to?) do |permission, p, global:| + .to receive(:allowed_to?) do |permission, p| permission[:controller] == 'calendar/calendars' && permission[:action] == 'index' && (p.nil? || p == project) end + allow(user).to receive(:allowed_to_globally?).and_return(false) end end @@ -60,7 +61,7 @@ describe ::Calendar::CalendarsController, type: :controller do it { is_expected.to render_template('calendar/calendars/index') } end - context 'project' do + context 'with project' do before do get :index, params: { project_id: project.id } end From d4e3bfa424bd6c91fbea3d31181ca735ede126aa Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Fri, 20 May 2022 11:27:45 +0200 Subject: [PATCH 50/80] delete AuthorizationService class to reduce indirections --- app/controllers/application_controller.rb | 2 +- app/models/user.rb | 10 ++--- app/services/authorization_service.rb | 49 ----------------------- lib/api/root_api.rb | 6 +-- 4 files changed, 7 insertions(+), 60 deletions(-) delete mode 100644 app/services/authorization_service.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5606ab455e..08a6b729b1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -228,7 +228,7 @@ class ApplicationController < ActionController::Base # Authorize the user for the requested action def authorize(ctrl = params[:controller], action = params[:action], global = false) context = @project || @projects - is_authorized = AuthorizationService.new({ controller: ctrl, action: action }, context: context, global: global).call + is_authorized = User.current.allowed_to?({ controller: ctrl, action: }, context, global:) unless is_authorized if @project&.archived? diff --git a/app/models/user.rb b/app/models/user.rb index 5772aa38e4..50bdf4b80a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -164,7 +164,7 @@ class User < Principal def reload(*args) @name = nil @projects_by_role = nil - @authorization_service = nil + @user_allowed_service = nil @project_role_cache = nil super @@ -519,7 +519,7 @@ class User < Principal end def allowed_to?(action, context, global: false) - authorization_service.call(action, context, global:).result + user_allowed_service.call(action, context, global:).result end def allowed_to_in_project?(action, project) @@ -530,7 +530,7 @@ class User < Principal allowed_to?(action, nil, global: true) end - delegate :preload_projects_allowed_to, to: :authorization_service + delegate :preload_projects_allowed_to, to: :user_allowed_service def reported_work_package_count WorkPackage.on_active_project.with_author(self).visible.count @@ -648,8 +648,8 @@ class User < Principal [skip_suffix_check, regexp] end - def authorization_service - @authorization_service ||= ::Authorization::UserAllowedService.new(self, role_cache: project_role_cache) + def user_allowed_service + @user_allowed_service ||= ::Authorization::UserAllowedService.new(self, role_cache: project_role_cache) end def project_role_cache diff --git a/app/services/authorization_service.rb b/app/services/authorization_service.rb deleted file mode 100644 index 3e016c642e..0000000000 --- a/app/services/authorization_service.rb +++ /dev/null @@ -1,49 +0,0 @@ -#-- 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. -#++ - -# project, projects, global, user = nil - -class AuthorizationService - # @params - # ctrl - controller - # action - action - # @named params - # context - single project or array of projects - default nil - # global - global - default false - # user - user - default current user - def initialize(permission, context: nil, global: false, user: User.current) - @permission = permission - @context = context - @global = global - @user = user - end - - def call - @user.allowed_to?(@permission, @context, global: @global) - end -end diff --git a/lib/api/root_api.rb b/lib/api/root_api.rb index 6a87e1706e..19dcd837ba 100644 --- a/lib/api/root_api.rb +++ b/lib/api/root_api.rb @@ -136,11 +136,7 @@ module API # # @raise [API::Errors::Unauthorized] when permission is not met def authorize(permission, context: nil, global: false, user: current_user, &block) - auth_service = AuthorizationService.new(permission, - context:, - global:, - user:) - + auth_service = -> { user.allowed_to?(permission, context, global:) } authorize_by_with_raise auth_service, &block end From 1e5c722909cb16ef15048270143d6ea9fa3b5140 Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Thu, 12 May 2022 10:52:29 +0200 Subject: [PATCH 52/80] target ruby version 3.1 in rubocop --- .rubocop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 6abcb51bc2..5ddc14858d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -3,7 +3,7 @@ require: - rubocop-rspec AllCops: - TargetRubyVersion: 3.0 + TargetRubyVersion: 3.1 # Enable any new cops in new versions by default NewCops: enable Exclude: From cc8fa1f7be36f3d9eafdb50b7e5a587222bf0416 Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Thu, 12 May 2022 11:36:17 +0200 Subject: [PATCH 53/80] implement GET /api/v3/days/week wp#41816 --- .../queries/week_days/week_day_query.rb | 30 +++++ app/models/week_day.rb | 6 + db/migrate/20220511124930_create_week_days.rb | 21 ++++ lib/api/decorators/single.rb | 2 - lib/api/v3/days/days_api.rb | 37 ++++++ lib/api/v3/days/week_api.rb | 40 +++++++ .../days/week_day_collection_representer.rb | 32 +++++ lib/api/v3/days/week_day_representer.rb | 48 ++++++++ lib/api/v3/root.rb | 3 +- lib/api/v3/utilities/path_helper.rb | 14 ++- spec/factories/week_day_factory.rb | 24 ++++ .../week_day_collection_representer_spec.rb | 44 +++++++ .../api/v3/days/week_day_representer_spec.rb | 110 ++++++++++++++++++ spec/models/week_day_spec.rb | 14 +++ spec/requests/api/v3/days/week_spec.rb | 50 ++++++++ 15 files changed, 469 insertions(+), 6 deletions(-) create mode 100644 app/models/queries/week_days/week_day_query.rb create mode 100644 app/models/week_day.rb create mode 100644 db/migrate/20220511124930_create_week_days.rb create mode 100644 lib/api/v3/days/days_api.rb create mode 100644 lib/api/v3/days/week_api.rb create mode 100644 lib/api/v3/days/week_day_collection_representer.rb create mode 100644 lib/api/v3/days/week_day_representer.rb create mode 100644 spec/factories/week_day_factory.rb create mode 100644 spec/lib/api/v3/days/week_day_collection_representer_spec.rb create mode 100644 spec/lib/api/v3/days/week_day_representer_spec.rb create mode 100644 spec/models/week_day_spec.rb create mode 100644 spec/requests/api/v3/days/week_spec.rb diff --git a/app/models/queries/week_days/week_day_query.rb b/app/models/queries/week_days/week_day_query.rb new file mode 100644 index 0000000000..4523c0cfde --- /dev/null +++ b/app/models/queries/week_days/week_day_query.rb @@ -0,0 +1,30 @@ +#-- 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. +#++ + +class Queries::WeekDays::WeekDayQuery < Queries::BaseQuery +end diff --git a/app/models/week_day.rb b/app/models/week_day.rb new file mode 100644 index 0000000000..ebc0b43a60 --- /dev/null +++ b/app/models/week_day.rb @@ -0,0 +1,6 @@ +class WeekDay < ApplicationRecord + def name + day_names = I18n.t('date.day_names') + day_names[day % 7] + end +end diff --git a/db/migrate/20220511124930_create_week_days.rb b/db/migrate/20220511124930_create_week_days.rb new file mode 100644 index 0000000000..d2f20ac40a --- /dev/null +++ b/db/migrate/20220511124930_create_week_days.rb @@ -0,0 +1,21 @@ +class CreateWeekDays < ActiveRecord::Migration[6.1] + def up + create_table :week_days do |t| + t.integer :day, null: false + t.boolean :working, null: false, default: true + + t.timestamps + end + + execute <<-SQL.squish + ALTER TABLE week_days + ADD CONSTRAINT unique_day_number UNIQUE (day); + ALTER TABLE week_days + ADD CHECK (day >= 1 AND day <=7); + SQL + end + + def down + drop_table :week_days + end +end diff --git a/lib/api/decorators/single.rb b/lib/api/decorators/single.rb index e782cc9ba4..2d1dcb2c68 100644 --- a/lib/api/decorators/single.rb +++ b/lib/api/decorators/single.rb @@ -30,8 +30,6 @@ require 'roar/decorator' require 'roar/hypermedia' require 'roar/json/hal' -require 'api/v3/utilities/path_helper' - module API module Decorators class Single < ::Roar::Decorator diff --git a/lib/api/v3/days/days_api.rb b/lib/api/v3/days/days_api.rb new file mode 100644 index 0000000000..efc828d06e --- /dev/null +++ b/lib/api/v3/days/days_api.rb @@ -0,0 +1,37 @@ +#-- 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::V3::Days + class DaysAPI < ::API::OpenProjectAPI + helpers ::API::Utilities::UrlPropsParsingHelper + + resources :days do + mount WeekAPI + end + end +end diff --git a/lib/api/v3/days/week_api.rb b/lib/api/v3/days/week_api.rb new file mode 100644 index 0000000000..41e6aa23fe --- /dev/null +++ b/lib/api/v3/days/week_api.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. +#++ + +module API::V3::Days + class WeekAPI < ::API::OpenProjectAPI + helpers ::API::Utilities::UrlPropsParsingHelper + + resources :week do + get &::API::V3::Utilities::Endpoints::Index.new(model: WeekDay, + render_representer: WeekDayCollectionRepresenter, + self_path: -> { api_v3_paths.days_week }) + .mount + end + end +end diff --git a/lib/api/v3/days/week_day_collection_representer.rb b/lib/api/v3/days/week_day_collection_representer.rb new file mode 100644 index 0000000000..eade97b5b4 --- /dev/null +++ b/lib/api/v3/days/week_day_collection_representer.rb @@ -0,0 +1,32 @@ +#-- 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::V3::Days + class WeekDayCollectionRepresenter < ::API::Decorators::UnpaginatedCollection + end +end diff --git a/lib/api/v3/days/week_day_representer.rb b/lib/api/v3/days/week_day_representer.rb new file mode 100644 index 0000000000..45f2f90a2d --- /dev/null +++ b/lib/api/v3/days/week_day_representer.rb @@ -0,0 +1,48 @@ +#-- 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::V3::Days + class WeekDayRepresenter < ::API::Decorators::Single + include ::API::Caching::CachedRepresenter + + property :day, render_nil: true + property :name, render_nil: true + property :working + + link :self do + { + href: api_v3_paths.days_week_day(represented.day), + title: represented.name + } + end + + def _type + 'WeekDay' + end + end +end diff --git a/lib/api/v3/root.rb b/lib/api/v3/root.rb index dddbe27ede..db5199e780 100644 --- a/lib/api/v3/root.rb +++ b/lib/api/v3/root.rb @@ -52,6 +52,7 @@ module API mount ::API::V3::Configuration::ConfigurationAPI mount ::API::V3::CustomActions::CustomActionsAPI mount ::API::V3::CustomOptions::CustomOptionsAPI + mount ::API::V3::Days::DaysAPI mount ::API::V3::Notifications::NotificationsAPI mount ::API::V3::HelpTexts::HelpTextsAPI mount ::API::V3::Memberships::MembershipsAPI @@ -80,7 +81,7 @@ module API mount ::API::V3::Grids::GridsAPI get '/' do - RootRepresenter.new({}, current_user: current_user) + RootRepresenter.new({}, current_user:) end get '/spec.json' do diff --git a/lib/api/v3/utilities/path_helper.rb b/lib/api/v3/utilities/path_helper.rb index c8e0e27b05..f998da95d6 100644 --- a/lib/api/v3/utilities/path_helper.rb +++ b/lib/api/v3/utilities/path_helper.rb @@ -192,6 +192,14 @@ module API "#{root}/custom_options/#{id}" end + def self.days_week + "#{root}/days/week" + end + + def self.days_week_day(day) + "#{days_week}/#{day}" + end + index :help_text show :help_text @@ -473,7 +481,7 @@ module API "#{project_id}-#{type_id}" end - filter = [{ id: { operator: '=', values: values } }] + filter = [{ id: { operator: '=', values: } }] path + "?filters=#{CGI.escape(filter.to_s)}" end @@ -497,8 +505,8 @@ module API sortBy: sort_by&.to_json, groupBy: group_by, pageSize: page_size, - offset: offset, - select: select + offset:, + select: }.compact_blank if query_params.any? diff --git a/spec/factories/week_day_factory.rb b/spec/factories/week_day_factory.rb new file mode 100644 index 0000000000..e8c7e45015 --- /dev/null +++ b/spec/factories/week_day_factory.rb @@ -0,0 +1,24 @@ +FactoryBot.define do + factory :week_day do + sequence :day, [1, 2, 3, 4, 5, 6, 7].cycle + working { day < 6 } + + # hack to reuse the day if it already exists in database + to_create do |instance| + instance.attributes = WeekDay.find_or_create_by(instance.attributes.slice("day", "working")).attributes + instance.instance_variable_set('@new_record', false) + end + end + + # Factory to create all 7 week days at once + factory :week_days, class: 'Array' do + # Skip the create callback to be able to use non-AR models. Otherwise FactoryBot will + # try to call #save! on any created object. + skip_create + + initialize_with do + days = 1.upto(7).map { |day| create(:week_day, day:) } + new(days) + end + end +end diff --git a/spec/lib/api/v3/days/week_day_collection_representer_spec.rb b/spec/lib/api/v3/days/week_day_collection_representer_spec.rb new file mode 100644 index 0000000000..af716f91db --- /dev/null +++ b/spec/lib/api/v3/days/week_day_collection_representer_spec.rb @@ -0,0 +1,44 @@ +#-- 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 ::API::V3::Days::WeekDayCollectionRepresenter do + let(:week_days) { build(:week_days) } + let(:representer) do + described_class.new(week_days, + self_link: '/api/v3/days/week', + current_user: instance_double(User, name: 'current_user')) + end + + describe '#to_json' do + subject(:collection) { representer.to_json } + + it_behaves_like 'unpaginated APIv3 collection', 7, 'days/week', 'WeekDay' + end +end diff --git a/spec/lib/api/v3/days/week_day_representer_spec.rb b/spec/lib/api/v3/days/week_day_representer_spec.rb new file mode 100644 index 0000000000..82e955f606 --- /dev/null +++ b/spec/lib/api/v3/days/week_day_representer_spec.rb @@ -0,0 +1,110 @@ +#-- 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 ::API::V3::Days::WeekDayRepresenter do + let(:week_day) { build_stubbed(:week_day, day: 1) } + let(:representer) { described_class.new(week_day, current_user: instance_double(User, name: 'current_user')) } + + describe '#to_json' do + subject(:generated) { representer.to_json } + + it 'has _type: WeekDay' do + expect(subject).to be_json_eql('WeekDay'.to_json).at_path('_type') + end + + it 'has day integer property' do + expect(subject).to have_json_type(Integer).at_path('day') + expect(subject).to be_json_eql(week_day.day.to_json).at_path('day') + end + + it 'has name string property' do + expect(subject).to have_json_type(String).at_path('name') + expect(subject).to be_json_eql(week_day.name.to_json).at_path('name') + end + + it 'has working boolean property' do + expect(subject).to have_json_type(TrueClass).at_path('working') + expect(subject).to be_json_eql(week_day.working.to_json).at_path('working') + end + + describe '_links' do + it 'is present' do + expect(subject).to have_json_type(Object).at_path('_links') + end + + describe 'self' do + it 'links to this resource' do + expected_json = { + href: "/api/v3/days/week/#{week_day.day}", + title: week_day.name + }.to_json + expect(subject).to be_json_eql(expected_json).at_path('_links/self') + end + end + end + end + + describe 'caching' do + it 'is based on the representer\'s json_cache_key' do + allow(OpenProject::Cache) + .to receive(:fetch) + .and_call_original + + representer.to_json + + expect(OpenProject::Cache) + .to have_received(:fetch) + .with(representer.json_cache_key) + end + + describe '#json_cache_key' do + let!(:former_cache_key) { representer.json_cache_key } + + it 'includes the name of the representer class' do + expect(representer.json_cache_key) + .to include('API', 'V3', 'Days', 'WeekDayRepresenter') + end + + it 'changes when the locale changes' do + I18n.with_locale(:fr) do + expect(representer.json_cache_key) + .not_to eql former_cache_key + end + end + + it 'changes when the week_day is updated' do + week_day.updated_at = 20.seconds.from_now + + expect(representer.json_cache_key) + .not_to eql former_cache_key + end + end + end +end diff --git a/spec/models/week_day_spec.rb b/spec/models/week_day_spec.rb new file mode 100644 index 0000000000..a239501265 --- /dev/null +++ b/spec/models/week_day_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +RSpec.describe WeekDay, type: :model do + describe '#name' do + it 'returns the translated week day name' do + expect(described_class.create(day: 1).name).to eq('Monday') + expect(described_class.create(day: 7).name).to eq('Sunday') + I18n.with_locale(:de) do + expect(described_class.create(day: 3).name).to eq('Mittwoch') + expect(described_class.create(day: 4).name).to eq('Donnerstag') + end + end + end +end diff --git a/spec/requests/api/v3/days/week_spec.rb b/spec/requests/api/v3/days/week_spec.rb new file mode 100644 index 0000000000..82c6336922 --- /dev/null +++ b/spec/requests/api/v3/days/week_spec.rb @@ -0,0 +1,50 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2020 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. + +require 'spec_helper' + +describe ::API::V3::Days::WeekAPI, + 'index', + content_type: :json, + type: :request do + include API::V3::Utilities::PathHelper + + let(:parsed_response) { JSON.parse(last_response.body) } + + current_user { user } + + before do + create(:week_days) + get api_v3_paths.days_week + end + + context 'for an admin user' do + let(:user) { build(:admin) } + + it_behaves_like 'API V3 collection response', 7, 7, 'WeekDay' + end +end From 880e1a92ddc958f21ec08557143e1cf339d83cd4 Mon Sep 17 00:00:00 2001 From: Dombi Attila Date: Mon, 16 May 2022 17:03:09 +0300 Subject: [PATCH 54/80] Implement GET /api/v3/days/week/{day} --- lib/api/v3/days/week_api.rb | 7 ++ .../api/v3/days/show_resource_spec.rb | 73 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 spec/requests/api/v3/days/show_resource_spec.rb diff --git a/lib/api/v3/days/week_api.rb b/lib/api/v3/days/week_api.rb index 41e6aa23fe..777dad4f91 100644 --- a/lib/api/v3/days/week_api.rb +++ b/lib/api/v3/days/week_api.rb @@ -35,6 +35,13 @@ module API::V3::Days render_representer: WeekDayCollectionRepresenter, self_path: -> { api_v3_paths.days_week }) .mount + route_param :day, type: Integer, desc: 'WeekDay ID' do + after_validation do + @week_day = WeekDay.find_by!(day: declared_params[:day]) + end + + get &::API::V3::Utilities::Endpoints::Show.new(model: WeekDay, render_representer: WeekDayRepresenter).mount + end end end end diff --git a/spec/requests/api/v3/days/show_resource_spec.rb b/spec/requests/api/v3/days/show_resource_spec.rb new file mode 100644 index 0000000000..dffb12dc1a --- /dev/null +++ b/spec/requests/api/v3/days/show_resource_spec.rb @@ -0,0 +1,73 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2020 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. + +require 'spec_helper' + +describe ::API::V3::Days::WeekAPI, + 'show', + content_type: :json, + type: :request do + include API::V3::Utilities::PathHelper + + let(:week_day) { create(:week_day, day: 1) } + let(:path) { api_v3_paths.days_week_day(week_day.day) } + + current_user { user } + subject { last_response.body } + + before do + get path + end + + context 'for an admin user' do + let(:user) { build(:admin) } + + it_behaves_like 'successful response' + + it 'responds with the correct day' do + expect(subject).to be_json_eql('WeekDay'.to_json).at_path('_type') + expect(subject).to be_json_eql(1.to_json).at_path('day') + end + + context 'when requesting nonexistent day' do + let(:path) { api_v3_paths.days_week_day(0) } + + it_behaves_like 'not found' + end + end + + context 'for a not logged in user' do + let(:user) { build(:anonymous) } + + it_behaves_like 'successful response' + + it 'responds with the correct day' do + expect(subject).to be_json_eql('WeekDay'.to_json).at_path('_type') + expect(subject).to be_json_eql(1.to_json).at_path('day') + end + end +end From 0ab1b8b8f5f66bdc6aa5898e18c976e0cf8504e1 Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Tue, 17 May 2022 10:16:23 +0200 Subject: [PATCH 55/80] use self_link helper in WeekDayRepresenter --- lib/api/v3/days/week_day_representer.rb | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/api/v3/days/week_day_representer.rb b/lib/api/v3/days/week_day_representer.rb index 45f2f90a2d..a1ec01d0ef 100644 --- a/lib/api/v3/days/week_day_representer.rb +++ b/lib/api/v3/days/week_day_representer.rb @@ -30,16 +30,11 @@ module API::V3::Days class WeekDayRepresenter < ::API::Decorators::Single include ::API::Caching::CachedRepresenter - property :day, render_nil: true - property :name, render_nil: true + property :day + property :name property :working - link :self do - { - href: api_v3_paths.days_week_day(represented.day), - title: represented.name - } - end + self_link path: :days_week_day, id_attribute: :day def _type 'WeekDay' From e7bbad61ec29c39f676b88063fea37cf63df75bd Mon Sep 17 00:00:00 2001 From: Dombi Attila Date: Tue, 17 May 2022 11:53:06 +0300 Subject: [PATCH 56/80] Add the WeekDaySeeder --- app/seeders/basic_data/week_day_seeder.rb | 58 +++++++++++++++++++ .../standard_seeder/basic_data_seeder.rb | 1 + spec/seeders/week_day_seeder_spec.rb | 52 +++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 app/seeders/basic_data/week_day_seeder.rb create mode 100644 spec/seeders/week_day_seeder_spec.rb diff --git a/app/seeders/basic_data/week_day_seeder.rb b/app/seeders/basic_data/week_day_seeder.rb new file mode 100644 index 0000000000..81d98bd285 --- /dev/null +++ b/app/seeders/basic_data/week_day_seeder.rb @@ -0,0 +1,58 @@ +#-- 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 BasicData + class WeekDaySeeder < Seeder + def seed_data! + WeekDay.transaction do + days.each do |attributes| + WeekDay.create!(attributes) + end + end + end + + def applicable? + WeekDay.none? + end + + def not_applicable_message + 'Skipping week days as there are already some configured' + end + + def days + [ + { day: 1, working: true }, + { day: 2, working: true }, + { day: 3, working: true }, + { day: 4, working: true }, + { day: 5, working: true }, + { day: 6, working: false }, + { day: 7, working: false } + ] + end + end +end diff --git a/app/seeders/standard_seeder/basic_data_seeder.rb b/app/seeders/standard_seeder/basic_data_seeder.rb index 4ca670082c..06a404f24b 100644 --- a/app/seeders/standard_seeder/basic_data_seeder.rb +++ b/app/seeders/standard_seeder/basic_data_seeder.rb @@ -31,6 +31,7 @@ module StandardSeeder [ ::BasicData::BuiltinRolesSeeder, ::BasicData::RoleSeeder, + ::BasicData::WeekDaySeeder, ::StandardSeeder::BasicData::ActivitySeeder, ::BasicData::ColorSeeder, ::BasicData::ColorSchemeSeeder, diff --git a/spec/seeders/week_day_seeder_spec.rb b/spec/seeders/week_day_seeder_spec.rb new file mode 100644 index 0000000000..fc26f1c11e --- /dev/null +++ b/spec/seeders/week_day_seeder_spec.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' + +describe ::BasicData::WeekDaySeeder do + subject { described_class.new } + + before do + subject.seed! + end + + def reseed! + subject.seed! + end + + it 'creates the initial days' do + WeekDay.destroy_all + expect { reseed! }.to change { WeekDay.pluck(:day, :working) } + .from([]) + .to([[1, true], [2, true], [3, true], [4, true], [5, true], [6, false], [7, false]]) + end + + it 'does not override existing days' do + expect { reseed! }.not_to change { WeekDay.pluck(:day, :working) } + end +end From e92253aeb70d23cf3b65dde5fa3e9c2e58977bd7 Mon Sep 17 00:00:00 2001 From: Dombi Attila Date: Tue, 17 May 2022 15:18:44 +0300 Subject: [PATCH 57/80] Add non-working day show api endpoint --- app/models/non_working_day.rb | 4 + .../20220517113828_create_non_working_days.rb | 11 ++ lib/api/v3/days/days_api.rb | 1 + .../v3/days/non_working_day_representer.rb | 43 +++++++ lib/api/v3/days/non_working_days_api.rb | 45 ++++++++ lib/api/v3/utilities/path_helper.rb | 8 ++ spec/factories/non_working_day_factory.rb | 6 + .../days/non_working_day_representer_spec.rb | 105 ++++++++++++++++++ spec/models/non_working_day_spec.rb | 30 +++++ .../non_working_days_show_resource_spec.rb | 85 ++++++++++++++ ...rce_spec.rb => week_show_resource_spec.rb} | 0 11 files changed, 338 insertions(+) create mode 100644 app/models/non_working_day.rb create mode 100644 db/migrate/20220517113828_create_non_working_days.rb create mode 100644 lib/api/v3/days/non_working_day_representer.rb create mode 100644 lib/api/v3/days/non_working_days_api.rb create mode 100644 spec/factories/non_working_day_factory.rb create mode 100644 spec/lib/api/v3/days/non_working_day_representer_spec.rb create mode 100644 spec/models/non_working_day_spec.rb create mode 100644 spec/requests/api/v3/days/non_working_days_show_resource_spec.rb rename spec/requests/api/v3/days/{show_resource_spec.rb => week_show_resource_spec.rb} (100%) diff --git a/app/models/non_working_day.rb b/app/models/non_working_day.rb new file mode 100644 index 0000000000..b008a9103d --- /dev/null +++ b/app/models/non_working_day.rb @@ -0,0 +1,4 @@ +class NonWorkingDay < ApplicationRecord + validates :name, :date, presence: true + validates :date, uniqueness: true +end diff --git a/db/migrate/20220517113828_create_non_working_days.rb b/db/migrate/20220517113828_create_non_working_days.rb new file mode 100644 index 0000000000..6200142545 --- /dev/null +++ b/db/migrate/20220517113828_create_non_working_days.rb @@ -0,0 +1,11 @@ +class CreateNonWorkingDays < ActiveRecord::Migration[6.1] + def change + create_table :non_working_days do |t| + t.string :name, null: false + t.date :date, null: false + + t.timestamps + end + add_index :non_working_days, :date, unique: true + end +end diff --git a/lib/api/v3/days/days_api.rb b/lib/api/v3/days/days_api.rb index efc828d06e..cfbcf40ca7 100644 --- a/lib/api/v3/days/days_api.rb +++ b/lib/api/v3/days/days_api.rb @@ -32,6 +32,7 @@ module API::V3::Days resources :days do mount WeekAPI + mount NonWorkingDaysAPI end end end diff --git a/lib/api/v3/days/non_working_day_representer.rb b/lib/api/v3/days/non_working_day_representer.rb new file mode 100644 index 0000000000..4785c083ed --- /dev/null +++ b/lib/api/v3/days/non_working_day_representer.rb @@ -0,0 +1,43 @@ +#-- 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::V3::Days + class NonWorkingDayRepresenter < ::API::Decorators::Single + include ::API::Decorators::DateProperty + include ::API::Caching::CachedRepresenter + + property :name + date_property :date + + self_link path: :days_non_working_day, id_attribute: :date + + def _type + 'NonWorkingDay' + end + end +end diff --git a/lib/api/v3/days/non_working_days_api.rb b/lib/api/v3/days/non_working_days_api.rb new file mode 100644 index 0000000000..7233152d26 --- /dev/null +++ b/lib/api/v3/days/non_working_days_api.rb @@ -0,0 +1,45 @@ +#-- 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::V3::Days + class NonWorkingDaysAPI < ::API::OpenProjectAPI + helpers ::API::Utilities::UrlPropsParsingHelper + + resources :non_working do + route_param :date, type: Date, desc: 'NonWorkingDay DATE' do + after_validation do + @non_working_day = NonWorkingDay.find_by!(date: declared_params[:date]) + end + + get &::API::V3::Utilities::Endpoints::Show.new(model: NonWorkingDay, + render_representer: NonWorkingDayRepresenter) + .mount + end + end + end +end diff --git a/lib/api/v3/utilities/path_helper.rb b/lib/api/v3/utilities/path_helper.rb index f998da95d6..18c3061c91 100644 --- a/lib/api/v3/utilities/path_helper.rb +++ b/lib/api/v3/utilities/path_helper.rb @@ -200,6 +200,14 @@ module API "#{days_week}/#{day}" end + def self.days_non_working + "#{root}/days/non_working" + end + + def self.days_non_working_day(date) + "#{days_non_working}/#{date}" + end + index :help_text show :help_text diff --git a/spec/factories/non_working_day_factory.rb b/spec/factories/non_working_day_factory.rb new file mode 100644 index 0000000000..305b75b730 --- /dev/null +++ b/spec/factories/non_working_day_factory.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :non_working_day do + name { "MyString" } + sequence(:date) { |n| Time.zone.today + n.day } + end +end diff --git a/spec/lib/api/v3/days/non_working_day_representer_spec.rb b/spec/lib/api/v3/days/non_working_day_representer_spec.rb new file mode 100644 index 0000000000..13632928b6 --- /dev/null +++ b/spec/lib/api/v3/days/non_working_day_representer_spec.rb @@ -0,0 +1,105 @@ +#-- 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 ::API::V3::Days::NonWorkingDayRepresenter do + let(:non_working_day) { build_stubbed(:non_working_day, name: "Christmas day", date: Date.tomorrow) } + let(:representer) { described_class.new(non_working_day, current_user: instance_double(User, name: 'current_user')) } + + describe '#to_json' do + subject(:generated) { representer.to_json } + + it 'has _type: NonWorkingDay' do + expect(subject).to be_json_eql('NonWorkingDay'.to_json).at_path('_type') + end + + it 'has name string property' do + expect(subject).to have_json_type(String).at_path('name') + expect(subject).to be_json_eql(non_working_day.name.to_json).at_path('name') + end + + it_behaves_like 'has ISO 8601 date only' do + let(:date) { non_working_day.date } + let(:json_path) { 'date' } + end + + describe '_links' do + it 'is present' do + expect(subject).to have_json_type(Object).at_path('_links') + end + + describe 'self' do + it 'links to this resource' do + expected_json = { + href: "/api/v3/days/non_working/#{non_working_day.date}", + title: non_working_day.name + }.to_json + expect(subject).to be_json_eql(expected_json).at_path('_links/self') + end + end + end + end + + describe 'caching' do + it 'is based on the representer\'s json_cache_key' do + allow(OpenProject::Cache) + .to receive(:fetch) + .and_call_original + + representer.to_json + + expect(OpenProject::Cache) + .to have_received(:fetch) + .with(representer.json_cache_key) + end + + describe '#json_cache_key' do + let!(:former_cache_key) { representer.json_cache_key } + + it 'includes the name of the representer class' do + expect(representer.json_cache_key) + .to include('API', 'V3', 'Days', 'NonWorkingDayRepresenter') + end + + it 'changes when the locale changes' do + I18n.with_locale(:fr) do + expect(representer.json_cache_key) + .not_to eql former_cache_key + end + end + + it 'changes when the non_working_day is updated' do + non_working_day.updated_at = 20.seconds.from_now + + expect(representer.json_cache_key) + .not_to eql former_cache_key + end + end + end +end diff --git a/spec/models/non_working_day_spec.rb b/spec/models/non_working_day_spec.rb new file mode 100644 index 0000000000..aa322e7bba --- /dev/null +++ b/spec/models/non_working_day_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +RSpec.describe NonWorkingDay, type: :model do + subject { build(:non_working_day) } + + describe 'validations' do + it 'is valid when all attributes are present' do + expect(subject).to be_valid + end + + it 'is invalid without name' do + subject.name = nil + expect(subject).to be_invalid + expect(subject.errors[:name]).to be_present + end + + it 'is invalid without date' do + subject.date = nil + expect(subject).to be_invalid + expect(subject.errors[:date]).to be_present + end + + it 'is invalid with an already existing date' do + existing = create(:non_working_day) + subject.date = existing.date + expect(subject).to be_invalid + expect(subject.errors[:date]).to be_present + end + end +end diff --git a/spec/requests/api/v3/days/non_working_days_show_resource_spec.rb b/spec/requests/api/v3/days/non_working_days_show_resource_spec.rb new file mode 100644 index 0000000000..024d65205f --- /dev/null +++ b/spec/requests/api/v3/days/non_working_days_show_resource_spec.rb @@ -0,0 +1,85 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2020 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. + +require 'spec_helper' + +describe ::API::V3::Days::NonWorkingDaysAPI, + 'show', + content_type: :json, + type: :request do + include API::V3::Utilities::PathHelper + + let(:non_working_day) { create(:non_working_day) } + let(:path) { api_v3_paths.days_non_working_day(non_working_day.date) } + + current_user { user } + subject { last_response.body } + + before do + get path + end + + context 'for an admin user' do + let(:user) { build(:admin) } + + it_behaves_like 'successful response' + + it 'responds with the correct day' do + expect(subject).to be_json_eql('NonWorkingDay'.to_json).at_path('_type') + expect(subject).to be_json_eql(non_working_day.date.to_json).at_path('date') + end + + context 'when requesting nonexistent date' do + let(:path) { api_v3_paths.days_non_working_day(Time.zone.today) } + + it_behaves_like 'not found' + end + + context 'when requesting an incorrect date' do + let(:path) { api_v3_paths.days_non_working_day("incorrect") } + + it_behaves_like 'param validation error' do + let(:id) { 'incorrect' } + end + end + end + + context 'for a not logged in user' do + let(:user) { build(:anonymous) } + + it_behaves_like 'successful response' + + it 'responds with the correct _type' do + expect(subject).to be_json_eql('NonWorkingDay'.to_json).at_path('_type') + end + + it 'responds with the correct day' do + expect(subject).to be_json_eql('NonWorkingDay'.to_json).at_path('_type') + expect(subject).to be_json_eql(non_working_day.date.to_json).at_path('date') + end + end +end diff --git a/spec/requests/api/v3/days/show_resource_spec.rb b/spec/requests/api/v3/days/week_show_resource_spec.rb similarity index 100% rename from spec/requests/api/v3/days/show_resource_spec.rb rename to spec/requests/api/v3/days/week_show_resource_spec.rb From 2471b069754d10364f4997a02ecfe9a4239b699d Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Tue, 17 May 2022 15:15:57 +0200 Subject: [PATCH 58/80] #41816: add stubs of days representer the links are still missing and will be added later --- app/models/day.rb | 33 ++++++++ lib/api/v3/days/day_collection_representer.rb | 32 ++++++++ lib/api/v3/days/day_representer.rb | 43 +++++++++++ lib/api/v3/utilities/path_helper.rb | 10 ++- spec/factories/day_factory.rb | 37 +++++++++ spec/factories/week_day_factory.rb | 28 +++++++ .../days/day_collection_representer_spec.rb | 51 +++++++++++++ spec/lib/api/v3/days/day_representer_spec.rb | 76 +++++++++++++++++++ 8 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 app/models/day.rb create mode 100644 lib/api/v3/days/day_collection_representer.rb create mode 100644 lib/api/v3/days/day_representer.rb create mode 100644 spec/factories/day_factory.rb create mode 100644 spec/lib/api/v3/days/day_collection_representer_spec.rb create mode 100644 spec/lib/api/v3/days/day_representer_spec.rb diff --git a/app/models/day.rb b/app/models/day.rb new file mode 100644 index 0000000000..78807171cb --- /dev/null +++ b/app/models/day.rb @@ -0,0 +1,33 @@ +#-- 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. +#++ + +class Day + include ActiveModel::Model + + attr_accessor :date, :name, :working +end diff --git a/lib/api/v3/days/day_collection_representer.rb b/lib/api/v3/days/day_collection_representer.rb new file mode 100644 index 0000000000..8cf31640f9 --- /dev/null +++ b/lib/api/v3/days/day_collection_representer.rb @@ -0,0 +1,32 @@ +#-- 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::V3::Days + class DayCollectionRepresenter < ::API::Decorators::UnpaginatedCollection + end +end diff --git a/lib/api/v3/days/day_representer.rb b/lib/api/v3/days/day_representer.rb new file mode 100644 index 0000000000..03229601d1 --- /dev/null +++ b/lib/api/v3/days/day_representer.rb @@ -0,0 +1,43 @@ +#-- 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::V3::Days + class DayRepresenter < ::API::Decorators::Single + # include ::API::Caching::CachedRepresenter + + property :date + property :name + property :working + + self_link path: :day, id_attribute: :date + + def _type + 'Day' + end + end +end diff --git a/lib/api/v3/utilities/path_helper.rb b/lib/api/v3/utilities/path_helper.rb index 18c3061c91..79176c373f 100644 --- a/lib/api/v3/utilities/path_helper.rb +++ b/lib/api/v3/utilities/path_helper.rb @@ -192,8 +192,16 @@ module API "#{root}/custom_options/#{id}" end + def self.day(date) + "#{days}/#{date}" + end + + def self.days + "#{root}/days" + end + def self.days_week - "#{root}/days/week" + "#{days}/week" end def self.days_week_day(day) diff --git a/spec/factories/day_factory.rb b/spec/factories/day_factory.rb new file mode 100644 index 0000000000..706056ba73 --- /dev/null +++ b/spec/factories/day_factory.rb @@ -0,0 +1,37 @@ +#-- 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. +#++ + +FactoryBot.define do + factory :day, class: 'Day' do + sequence :date do |n| + (1.year.ago + n.days).to_date + end + name { WeekDay.new(day: date.wday).name } + working { date.wday < 6 } + end +end diff --git a/spec/factories/week_day_factory.rb b/spec/factories/week_day_factory.rb index e8c7e45015..1097485742 100644 --- a/spec/factories/week_day_factory.rb +++ b/spec/factories/week_day_factory.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. +#++ + FactoryBot.define do factory :week_day do sequence :day, [1, 2, 3, 4, 5, 6, 7].cycle diff --git a/spec/lib/api/v3/days/day_collection_representer_spec.rb b/spec/lib/api/v3/days/day_collection_representer_spec.rb new file mode 100644 index 0000000000..22773be52c --- /dev/null +++ b/spec/lib/api/v3/days/day_collection_representer_spec.rb @@ -0,0 +1,51 @@ +#-- 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 ::API::V3::Days::DayCollectionRepresenter do + let(:days) do + [ + build(:day, date: Date.new(2022, 12, 27)), + build(:day, date: Date.new(2022, 12, 28)), + build(:day, date: Date.new(2022, 12, 29)) + ] + end + let(:current_user) { instance_double(User, name: 'current_user') } + let(:representer) do + described_class.new(days, + self_link: '/api/v3/self_link_untested', + current_user:) + end + + describe '#to_json' do + subject(:collection) { representer.to_json } + + it_behaves_like 'unpaginated APIv3 collection', 3, 'self_link_untested', 'Day' + end +end diff --git a/spec/lib/api/v3/days/day_representer_spec.rb b/spec/lib/api/v3/days/day_representer_spec.rb new file mode 100644 index 0000000000..cdce648808 --- /dev/null +++ b/spec/lib/api/v3/days/day_representer_spec.rb @@ -0,0 +1,76 @@ +#-- 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 ::API::V3::Days::DayRepresenter do + let(:day) do + build(:day, date: Date.new(2022, 12, 27), name: 'Tuesday', working: true) + end + let(:current_user) { instance_double(User, name: 'current_user') } + let(:representer) { described_class.new(day, current_user:) } + + describe '#to_json' do + subject(:generated) { representer.to_json } + + it 'has _type: Day' do + expect(subject).to be_json_eql('Day'.to_json).at_path('_type') + end + + it 'has date property' do + expect(subject).to have_json_type(String).at_path('date') + expect(subject).to be_json_eql('2022-12-27'.to_json).at_path('date') + end + + it 'has name string property' do + expect(subject).to have_json_type(String).at_path('name') + expect(subject).to be_json_eql(day.name.to_json).at_path('name') + end + + it 'has working boolean property' do + expect(subject).to have_json_type(TrueClass).at_path('working') + expect(subject).to be_json_eql(day.working.to_json).at_path('working') + end + + describe '_links' do + it 'is present' do + expect(subject).to have_json_type(Object).at_path('_links') + end + + describe 'self' do + it 'links to this resource' do + expected_json = { + href: '/api/v3/days/2022-12-27', + title: 'Tuesday' + }.to_json + expect(subject).to be_json_eql(expected_json).at_path('_links/self') + end + end + end + end +end From 084df4233471b8ef7b00482c3f3d80a472f70691 Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Wed, 18 May 2022 11:08:57 +0200 Subject: [PATCH 59/80] add first implementation of /api/v3/days no filters yet --- app/models/day.rb | 41 +++++++++++++-- app/models/queries/days/day_query.rb | 41 +++++++++++++++ lib/api/v3/days/days_api.rb | 8 ++- spec/factories/day_factory.rb | 2 +- spec/factories/week_day_factory.rb | 4 ++ .../days/day_collection_representer_spec.rb | 1 + spec/lib/api/v3/days/day_representer_spec.rb | 2 +- spec/requests/api/v3/days/day_spec.rb | 50 +++++++++++++++++++ 8 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 app/models/queries/days/day_query.rb create mode 100644 spec/requests/api/v3/days/day_spec.rb diff --git a/app/models/day.rb b/app/models/day.rb index 78807171cb..5c98e02080 100644 --- a/app/models/day.rb +++ b/app/models/day.rb @@ -26,8 +26,43 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Day - include ActiveModel::Model +class Day < ApplicationRecord + include Tableless - attr_accessor :date, :name, :working + belongs_to :week_day, + inverse_of: false, + class_name: 'WeekDay', + foreign_key: :day_of_week, + primary_key: :day + + attribute :date, :date, default: nil + attribute :day_of_week, :integer, default: nil + attribute :working, :boolean, default: 't' + + delegate :name, to: :week_day + + def self.default + today = Time.zone.today + from = today.at_beginning_of_month + to = today.next_month.at_end_of_month + + days_sql = <<~SQL.squish + ( + SELECT + date_trunc('day', dd)::date date, + extract(isodow from dd) day_of_week, + week_days.working + FROM generate_series + ( '#{from}'::timestamp, + '#{to}'::timestamp, + '1 day'::interval) dd + LEFT JOIN week_days + ON extract(isodow from dd) = week_days.day + ORDER BY date + ) days + SQL + + select('days.*') + .from(days_sql) + end end diff --git a/app/models/queries/days/day_query.rb b/app/models/queries/days/day_query.rb new file mode 100644 index 0000000000..2b8eba8227 --- /dev/null +++ b/app/models/queries/days/day_query.rb @@ -0,0 +1,41 @@ +#-- 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. +#++ + +class Queries::Days::DayQuery < Queries::BaseQuery + def self.model + Day + end + + def default_scope + Day.default + end + + def results + super.reorder('date ASC') + end +end diff --git a/lib/api/v3/days/days_api.rb b/lib/api/v3/days/days_api.rb index cfbcf40ca7..81fcef3fdf 100644 --- a/lib/api/v3/days/days_api.rb +++ b/lib/api/v3/days/days_api.rb @@ -31,8 +31,14 @@ module API::V3::Days helpers ::API::Utilities::UrlPropsParsingHelper resources :days do - mount WeekAPI mount NonWorkingDaysAPI + mount WeekAPI + + get &::API::V3::Utilities::Endpoints::Index.new( + model: Day, + render_representer: DayCollectionRepresenter, + self_path: -> { api_v3_paths.days } + ).mount end end end diff --git a/spec/factories/day_factory.rb b/spec/factories/day_factory.rb index 706056ba73..38bb2fe93a 100644 --- a/spec/factories/day_factory.rb +++ b/spec/factories/day_factory.rb @@ -31,7 +31,7 @@ FactoryBot.define do sequence :date do |n| (1.year.ago + n.days).to_date end - name { WeekDay.new(day: date.wday).name } + day_of_week { date.wday } working { date.wday < 6 } end end diff --git a/spec/factories/week_day_factory.rb b/spec/factories/week_day_factory.rb index 1097485742..cd2229e5b7 100644 --- a/spec/factories/week_day_factory.rb +++ b/spec/factories/week_day_factory.rb @@ -36,6 +36,10 @@ FactoryBot.define do instance.attributes = WeekDay.find_or_create_by(instance.attributes.slice("day", "working")).attributes instance.instance_variable_set('@new_record', false) end + + trait :tuesday do + day { 2 } + end end # Factory to create all 7 week days at once diff --git a/spec/lib/api/v3/days/day_collection_representer_spec.rb b/spec/lib/api/v3/days/day_collection_representer_spec.rb index 22773be52c..8c3e5cf709 100644 --- a/spec/lib/api/v3/days/day_collection_representer_spec.rb +++ b/spec/lib/api/v3/days/day_collection_representer_spec.rb @@ -29,6 +29,7 @@ require 'spec_helper' describe ::API::V3::Days::DayCollectionRepresenter do + let!(:week_days) { create(:week_days) } let(:days) do [ build(:day, date: Date.new(2022, 12, 27)), diff --git a/spec/lib/api/v3/days/day_representer_spec.rb b/spec/lib/api/v3/days/day_representer_spec.rb index cdce648808..689254913c 100644 --- a/spec/lib/api/v3/days/day_representer_spec.rb +++ b/spec/lib/api/v3/days/day_representer_spec.rb @@ -30,7 +30,7 @@ require 'spec_helper' describe ::API::V3::Days::DayRepresenter do let(:day) do - build(:day, date: Date.new(2022, 12, 27), name: 'Tuesday', working: true) + build(:day, date: Date.new(2022, 12, 27), week_day: create(:week_day, :tuesday), working: true) end let(:current_user) { instance_double(User, name: 'current_user') } let(:representer) { described_class.new(day, current_user:) } diff --git a/spec/requests/api/v3/days/day_spec.rb b/spec/requests/api/v3/days/day_spec.rb new file mode 100644 index 0000000000..869c962ee6 --- /dev/null +++ b/spec/requests/api/v3/days/day_spec.rb @@ -0,0 +1,50 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2020 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. + +require 'spec_helper' + +describe ::API::V3::Days::DaysAPI, + content_type: :json, + type: :request do + include API::V3::Utilities::PathHelper + + let(:parsed_response) { JSON.parse(last_response.body) } + + current_user { user } + + before do + create(:week_days) + get api_v3_paths.days + end + + context 'for an admin user' do + let(:user) { build(:admin) } + + nb_days = Time.zone.today.end_of_month.day + Time.zone.today.next_month.end_of_month.day + it_behaves_like 'API V3 collection response', nb_days, nb_days, 'Day' + end +end From c9e4a7dd6e92074893ada9a15373547d5e930cd9 Mon Sep 17 00:00:00 2001 From: Dombi Attila Date: Wed, 18 May 2022 20:23:42 +0300 Subject: [PATCH 60/80] Add dates_interval and working filters for api/v3/days endpoint --- app/models/day.rb | 10 +- app/models/queries/days.rb | 34 ++++++ app/models/queries/days/day_query.rb | 14 ++- .../days/filters/dates_interval_filter.rb | 62 +++++++++++ app/models/queries/days/filters/day_filter.rb | 35 ++++++ .../queries/days/filters/working_filter.rb | 35 ++++++ app/models/queries/filters/base.rb | 15 ++- docs/api/apiv3/paths/days.yml | 6 +- spec/models/queries/days/day_query_spec.rb | 104 ++++++++++++++++++ .../filters/dates_interval_filter_spec.rb | 50 +++++++++ .../days/filters/working_filter_spec.rb | 36 ++++++ spec/requests/api/v3/days/day_spec.rb | 12 +- 12 files changed, 398 insertions(+), 15 deletions(-) create mode 100644 app/models/queries/days.rb create mode 100644 app/models/queries/days/filters/dates_interval_filter.rb create mode 100644 app/models/queries/days/filters/day_filter.rb create mode 100644 app/models/queries/days/filters/working_filter.rb create mode 100644 spec/models/queries/days/day_query_spec.rb create mode 100644 spec/models/queries/days/filters/dates_interval_filter_spec.rb create mode 100644 spec/models/queries/days/filters/working_filter_spec.rb diff --git a/app/models/day.rb b/app/models/day.rb index 5c98e02080..15b7750373 100644 --- a/app/models/day.rb +++ b/app/models/day.rb @@ -46,7 +46,12 @@ class Day < ApplicationRecord from = today.at_beginning_of_month to = today.next_month.at_end_of_month - days_sql = <<~SQL.squish + select('days.*') + .from(Arel.sql(from_sql(from:, to:))) + end + + def self.from_sql(from:, to:) + <<~SQL.squish ( SELECT date_trunc('day', dd)::date date, @@ -61,8 +66,5 @@ class Day < ApplicationRecord ORDER BY date ) days SQL - - select('days.*') - .from(days_sql) end end diff --git a/app/models/queries/days.rb b/app/models/queries/days.rb new file mode 100644 index 0000000000..51b3328c91 --- /dev/null +++ b/app/models/queries/days.rb @@ -0,0 +1,34 @@ +#-- 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 Queries::Days + ::Queries::Register.register(DayQuery) do + filter Filters::DatesIntervalFilter + filter Filters::WorkingFilter + end +end diff --git a/app/models/queries/days/day_query.rb b/app/models/queries/days/day_query.rb index 2b8eba8227..996c04e9f8 100644 --- a/app/models/queries/days/day_query.rb +++ b/app/models/queries/days/day_query.rb @@ -35,7 +35,19 @@ class Queries::Days::DayQuery < Queries::BaseQuery Day.default end + ## + # This override is necessary, the dates interval filter needs to adjust the + # `from` clause of the query. To update the `from` clause, we reverse merge the filters, + # otherwise the `from` clause of the filter is ignored. + def apply_filters(scope) + filters.each do |filter| + scope = filter.scope.merge(scope) + end + + scope + end + def results - super.reorder('date ASC') + super.reorder(date: :asc) end end diff --git a/app/models/queries/days/filters/dates_interval_filter.rb b/app/models/queries/days/filters/dates_interval_filter.rb new file mode 100644 index 0000000000..00511675f3 --- /dev/null +++ b/app/models/queries/days/filters/dates_interval_filter.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. +#++ + +class Queries::Days::Filters::DatesIntervalFilter < Queries::Days::Filters::DayFilter + include Queries::Operators::DateRangeClauses + + def type + :date + end + + def self.key + :date + end + + def from + from, to = values.map { |v| v.blank? ? nil : Date.parse(v) } + + # Both from and to cannot be blank at this point + if from.nil? + from = to.at_beginning_of_month + end + + if to.nil? + to = from.next_month.at_end_of_month + end + + model.from_sql(from:, to:) + end + + def type_strategy + @type_strategy ||= Queries::Filters::Strategies::DateInterval.new(self) + end + + def connection + ActiveRecord::Base::connection + end +end diff --git a/app/models/queries/days/filters/day_filter.rb b/app/models/queries/days/filters/day_filter.rb new file mode 100644 index 0000000000..93262cb6df --- /dev/null +++ b/app/models/queries/days/filters/day_filter.rb @@ -0,0 +1,35 @@ +#-- 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. +#++ + +class Queries::Days::Filters::DayFilter < Queries::Filters::Base + self.model = Day + + def human_name + model.human_attribute_name(name) + end +end diff --git a/app/models/queries/days/filters/working_filter.rb b/app/models/queries/days/filters/working_filter.rb new file mode 100644 index 0000000000..8997cb23ce --- /dev/null +++ b/app/models/queries/days/filters/working_filter.rb @@ -0,0 +1,35 @@ +#-- 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. +#++ + +class Queries::Days::Filters::WorkingFilter < Queries::Days::Filters::DayFilter + include Queries::Filters::Shared::BooleanFilter + + def self.key + :working + end +end diff --git a/app/models/queries/filters/base.rb b/app/models/queries/filters/base.rb index faf12008e6..a6664cebac 100644 --- a/app/models/queries/filters/base.rb +++ b/app/models/queries/filters/base.rb @@ -73,7 +73,7 @@ class Queries::Filters::Base def filter_instance_options values = filter_params.map { |key| [key, send(key)] } - initial_options.merge(Hash[values]) + initial_options.merge(values.to_h) end def human_name @@ -88,9 +88,7 @@ class Queries::Filters::Base nil end - def valid_values! - type_strategy.valid_values! - end + delegate :valid_values!, to: :type_strategy def available? true @@ -106,6 +104,7 @@ class Queries::Filters::Base def scope scope = model.where(where) + scope = scope.from(from) if from scope = scope.joins(joins) if joins scope = scope.left_outer_joins(left_outer_joins) if left_outer_joins scope @@ -120,13 +119,17 @@ class Queries::Filters::Base end def self.all_for(context = nil) - create!(name: key, context: context) + create!(name: key, context:) end def where operator_strategy.sql_for_field(values, self.class.model.table_name, self.class.key) end + def from + nil + end + def joins nil end @@ -194,7 +197,7 @@ class Queries::Filters::Base end def validate_presence_of_values - if operator_strategy&.requires_value? && (values.nil? || values.reject(&:blank?).empty?) + if operator_strategy&.requires_value? && (values.nil? || values.compact_blank.empty?) errors.add(:values, I18n.t('activerecord.errors.messages.blank')) end end diff --git a/docs/api/apiv3/paths/days.yml b/docs/api/apiv3/paths/days.yml index cff47067ea..b9499060db 100644 --- a/docs/api/apiv3/paths/days.yml +++ b/docs/api/apiv3/paths/days.yml @@ -19,11 +19,11 @@ get: Accepts the same format as returned by the [queries](https://www.openproject.org/docs/api/endpoints/queries/) endpoint. Currently supported filters are: - + interval: the inclusive date interval to scope days to look up. When + + date: the inclusive date interval to scope days to look up. When unspecified, default is from the beginning of current month to the end of following month. - Example: `{ "interval": { "operator": "<>d", "values": ["2022-05-02","2022-05-26"] } }` + Example: `{ "date": { "operator": "<>d", "values": ["2022-05-02","2022-05-26"] } }` would return days between May 5 and May 26 2022, inclusive. + working: when `true`, returns only the working days. When `false`, @@ -32,7 +32,7 @@ get: Example: `{ "working": { "operator": "=", "values": ["t"] } }` would exclude non-working days from the response. - example: '[{ "interval": { "operator": "<>d", "values": ["2022-05-02","2022-05-26"] } }, { "working": { "operator": "=", "values": ["f"] } }]' + example: '[{ "date": { "operator": "<>d", "values": ["2022-05-02","2022-05-26"] } }, { "working": { "operator": "=", "values": ["f"] } }]' required: false schema: type: string diff --git a/spec/models/queries/days/day_query_spec.rb b/spec/models/queries/days/day_query_spec.rb new file mode 100644 index 0000000000..9db6195b74 --- /dev/null +++ b/spec/models/queries/days/day_query_spec.rb @@ -0,0 +1,104 @@ +#-- 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 Queries::Days::DayQuery, type: :model do + let(:instance) { described_class.new } + let(:base_scope) { Day.default.order(date: :asc) } + let(:current_user) { build_stubbed(:admin) } + + before do + login_as(current_user) + end + + context 'without a filter' do + context 'as an admin' do + it 'is the same as getting all days' do + expect(instance.results.to_sql).to eql base_scope.to_sql + end + end + + context 'as a non admin' do + let(:current_user) { build_stubbed(:user) } + + it 'is the same as getting all days' do + expect(instance.results.to_sql).to eql base_scope.to_sql + end + end + end + + context 'with a date filter using the "<>d" operator' do + let(:date_range) { [from.iso8601, to.iso8601] } + + before do + instance.where('date', '<>d', date_range) + end + + context 'with dates within the default range' do + let(:from) { Time.zone.today } + let(:to) { 5.days.from_now.to_date } + let(:base_scope) { Day.default.from(Day.from_sql(from:, to:)).order(date: :asc) } + + it 'is the same as handwriting the query' do + # Expectation has to be weirdly specific to the logic of Queries::Operators::DateRangeClauses + expected = base_scope.where("days.date > ? AND days.date <= ?", + (from - 1.day).end_of_day, + to.end_of_day) + expect(instance.results.to_sql).to eql expected.to_sql + end + end + + context 'with dates out of the default range' do + let(:from) { 5.days.from_now.to_date } + let(:to) { 5.months.from_now.to_date } + + it 'returns all the days' do + expect(instance.results.size).to be 149 + end + + context 'with dates missing the to date' do + let(:date_range) { [from.iso8601, ""] } + + it 'returns days until the end of next month' do + expected_size = (from.next_month.at_end_of_month - from).to_i + 1 + expect(instance.results.size).to be expected_size + end + end + + context 'with dates missing the from date' do + let(:date_range) { ["", to.iso8601] } + + it 'returns days from the beginning of the month' do + expected_size = (to - to.at_beginning_of_month).to_i + 1 + expect(instance.results.size).to be expected_size + end + end + end + end +end diff --git a/spec/models/queries/days/filters/dates_interval_filter_spec.rb b/spec/models/queries/days/filters/dates_interval_filter_spec.rb new file mode 100644 index 0000000000..8b00f78393 --- /dev/null +++ b/spec/models/queries/days/filters/dates_interval_filter_spec.rb @@ -0,0 +1,50 @@ +#-- 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 Queries::Days::Filters::DatesIntervalFilter, type: :model do + it_behaves_like 'basic query filter' do + let(:type) { :date } + let(:class_key) { :date } + + describe '#available?' do + it 'is true' do + expect(instance).to be_available + end + end + + describe '#allowed_values' do + it 'is nil' do + expect(instance.allowed_values).to be_nil + end + end + + it_behaves_like 'non ar filter' + end +end diff --git a/spec/models/queries/days/filters/working_filter_spec.rb b/spec/models/queries/days/filters/working_filter_spec.rb new file mode 100644 index 0000000000..7705dfc998 --- /dev/null +++ b/spec/models/queries/days/filters/working_filter_spec.rb @@ -0,0 +1,36 @@ +#-- 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 Queries::Days::Filters::WorkingFilter, type: :model do + it_behaves_like 'boolean query filter' do + let(:model) { Day } + let(:attribute) { :working } + end +end diff --git a/spec/requests/api/v3/days/day_spec.rb b/spec/requests/api/v3/days/day_spec.rb index 869c962ee6..f86699dd0e 100644 --- a/spec/requests/api/v3/days/day_spec.rb +++ b/spec/requests/api/v3/days/day_spec.rb @@ -33,12 +33,13 @@ describe ::API::V3::Days::DaysAPI, include API::V3::Utilities::PathHelper let(:parsed_response) { JSON.parse(last_response.body) } + let(:filters) { [] } current_user { user } before do create(:week_days) - get api_v3_paths.days + get api_v3_paths.path_for :days, filters: end context 'for an admin user' do @@ -46,5 +47,14 @@ describe ::API::V3::Days::DaysAPI, nb_days = Time.zone.today.end_of_month.day + Time.zone.today.next_month.end_of_month.day it_behaves_like 'API V3 collection response', nb_days, nb_days, 'Day' + + context 'when filtering by date' do + let(:filters) do + [{ date: { operator: '<>d', + values: [Time.zone.today.iso8601, 5.days.from_now.to_date.iso8601] } }] + end + + it_behaves_like 'API V3 collection response', 6, 6, 'Day' + end end end From a916c75803575502a135b98cde083071abe5d112 Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Thu, 19 May 2022 17:00:30 +0200 Subject: [PATCH 61/80] fixup! avoid N+1 query for /api/v3/days endpoint --- app/models/day.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/day.rb b/app/models/day.rb index 15b7750373..f73b4258c4 100644 --- a/app/models/day.rb +++ b/app/models/day.rb @@ -47,6 +47,7 @@ class Day < ApplicationRecord to = today.next_month.at_end_of_month select('days.*') + .includes(:week_day) .from(Arel.sql(from_sql(from:, to:))) end From d1cb1058b9cb2be31d8d1698436536896353517e Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Thu, 19 May 2022 17:00:53 +0200 Subject: [PATCH 62/80] fixup! clean useless code --- lib/api/v3/days/day_representer.rb | 2 -- lib/api/v3/days/days_api.rb | 1 - 2 files changed, 3 deletions(-) diff --git a/lib/api/v3/days/day_representer.rb b/lib/api/v3/days/day_representer.rb index 03229601d1..08b0599c51 100644 --- a/lib/api/v3/days/day_representer.rb +++ b/lib/api/v3/days/day_representer.rb @@ -28,8 +28,6 @@ module API::V3::Days class DayRepresenter < ::API::Decorators::Single - # include ::API::Caching::CachedRepresenter - property :date property :name property :working diff --git a/lib/api/v3/days/days_api.rb b/lib/api/v3/days/days_api.rb index 81fcef3fdf..2fa0b4db9c 100644 --- a/lib/api/v3/days/days_api.rb +++ b/lib/api/v3/days/days_api.rb @@ -36,7 +36,6 @@ module API::V3::Days get &::API::V3::Utilities::Endpoints::Index.new( model: Day, - render_representer: DayCollectionRepresenter, self_path: -> { api_v3_paths.days } ).mount end From 2437722250eadf2ca32e4dcc7848788bb8f84c45 Mon Sep 17 00:00:00 2001 From: OpenProject Actions CI Date: Mon, 23 May 2022 03:31:05 +0000 Subject: [PATCH 63/80] update locales from crowdin [ci skip] --- config/locales/crowdin/js-az.yml | 98 ++++++++++++++++---------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/config/locales/crowdin/js-az.yml b/config/locales/crowdin/js-az.yml index f87e427422..7d295016ef 100644 --- a/config/locales/crowdin/js-az.yml +++ b/config/locales/crowdin/js-az.yml @@ -22,68 +22,68 @@ az: js: ajax: - hide: "Hide" - loading: "Loading…" - updating: "Updating…" + hide: "Gizlət" + loading: "Yüklənir…" + updating: "Yenilənir…" attachments: draggable_hint: | Drag on editor field to inline image or reference attachment. Closed editor fields will be opened while you keep dragging. autocomplete_select: placeholder: - multi: "Add \"%{name}\"" - single: "Select \"%{name}\"" - remove: "Remove %{name}" - active: "Active %{label} %{name}" + multi: "\"%{name}\" əlavə et" + single: "\"%{name}\" seç" + remove: "%{name} çıxart" + active: "Aktiv %{label} %{name}" backup: - attachments_disabled: Attachments may not be included since they exceed the maximum overall size allowed. You can change this via the configuration (requires a server restart). + attachments_disabled: İcazə verilən maksimal cəm sayını aşdığı üçün qoşmalar daxil edilməyə bilər. Bunu konfiqurasiya vasitəsilə dəyişdirə bilərsiniz (serverin yenidən başladılmasını tələb edir). info: > You can trigger a backup here. The process can take some time depending on the amount of data (especially attachments) you have. You will receive an email once it's ready. note: > - A new backup will override any previous one. Only a limited number of backups per day can be requested. - last_backup: Last backup - last_backup_from: Last backup from - title: Backup OpenProject - options: Options - include_attachments: Include attachments - download_backup: Download backup - request_backup: Request backup + Yeni bir nüsxələmə, əvvəlkini etibarsız edəcək. Gündəlik yalnız limitli sayda nüsxələmə tələb oluna bilər. + last_backup: Son nüsxələmə + last_backup_from: Son nüsxələmə + title: OpenProject nüsxələməsi + options: Seçimlər + include_attachments: Qoşmaları daxil et + download_backup: Nüsxəni endir + request_backup: Nüsxə tələb et close_popup_title: "Açılan pəncərəni bağla" - close_filter_title: "Close filter" - close_form_title: "Close form" - button_add_watcher: "Add watcher" - button_add: "Add" - button_back: "Back" - button_back_to_list_view: "Back to list view" - button_cancel: "Cancel" - button_close: "Close" - button_change_project: "Change project" - button_check_all: "Check all" - button_configure-form: "Configure form" - button_confirm: "Confirm" - button_continue: "Continue" - button_copy: "Copy" - button_copy_to_other_project: "Copy to other project" - button_custom-fields: "Custom fields" - button_delete: "Delete" + close_filter_title: "Filtr seç" + close_form_title: "Formu bağla" + button_add_watcher: "Nəzarətçi əlavə et" + button_add: "Əlavə et" + button_back: "Geri" + button_back_to_list_view: "Siyahı görünüşünə qayıt" + button_cancel: "İmtina" + button_close: "Bağla" + button_change_project: "Layihəni dəyişdir" + button_check_all: "Hamısını seç" + button_configure-form: "Formu konfiqurasiya et" + button_confirm: "Təsdiqlə" + button_continue: "Davam" + button_copy: "Kopyala" + button_copy_to_other_project: "Digər layihəyə kopyala" + button_custom-fields: "Özəl sahələr" + button_delete: "Sil" button_delete_watcher: "İzləyicini sil" button_details_view: "Təfsilat baxışı" - button_duplicate: "Duplicate" - button_edit: "Edit" - button_filter: "Filter" - button_collapse_all: "Collapse all" - button_expand_all: "Expand all" - button_advanced_filter: "Advanced filter" + button_duplicate: "Çoxalt" + button_edit: "Düzəliş et" + button_filter: "Filtr" + button_collapse_all: "Hamısını yığcamlaşdır" + button_expand_all: "Hamısını genişləndir" + button_advanced_filter: "Qabaqcıl filtr" button_list_view: "Siyahı baxışı" - button_show_view: "Fullscreen view" - button_log_time: "Log time" - button_more: "More" + button_show_view: "Tam ekran görünüşü" + button_log_time: "Jurnal vaxtı" + button_more: "Daha çox" button_open_details: "Təfsilat baxışını aç" - button_close_details: "Close details view" - button_open_fullscreen: "Open fullscreen view" - button_show_cards: "Show card view" - button_show_list: "Show list view" - button_quote: "Quote" - button_save: "Save" + button_close_details: "Təfsilatlar görünüşünü bağla" + button_open_fullscreen: "Tam ekran görünüşünü aç" + button_show_cards: "Kart görünüşünü göstər" + button_show_list: "Siyahı görünüşünü göstər" + button_quote: "Sitat" + button_save: "Saxla" button_settings: "Settings" button_uncheck_all: "Uncheck all" button_update: "Yeniləmə" @@ -147,7 +147,7 @@ az: code_block: button: 'Insert code snippet' title: 'Insert / edit Code snippet' - language: 'Formatting language' + language: 'Formatlama dili' language_hint: 'Enter the formatting language that will be used for highlighting (if supported).' dropdown: macros: 'Macros' From 9b919befc23d26b8c8544ead8535123ffa57c986 Mon Sep 17 00:00:00 2001 From: OpenProject Actions CI Date: Mon, 23 May 2022 03:34:35 +0000 Subject: [PATCH 64/80] update locales from crowdin [ci skip] --- config/locales/crowdin/js-az.yml | 100 +++++++++++++++---------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/config/locales/crowdin/js-az.yml b/config/locales/crowdin/js-az.yml index 0cf567ed7a..7d295016ef 100644 --- a/config/locales/crowdin/js-az.yml +++ b/config/locales/crowdin/js-az.yml @@ -22,71 +22,71 @@ az: js: ajax: - hide: "Hide" - loading: "Loading…" - updating: "Updating…" + hide: "Gizlət" + loading: "Yüklənir…" + updating: "Yenilənir…" attachments: draggable_hint: | Drag on editor field to inline image or reference attachment. Closed editor fields will be opened while you keep dragging. autocomplete_select: placeholder: - multi: "Add \"%{name}\"" - single: "Select \"%{name}\"" - remove: "Remove %{name}" - active: "Active %{label} %{name}" + multi: "\"%{name}\" əlavə et" + single: "\"%{name}\" seç" + remove: "%{name} çıxart" + active: "Aktiv %{label} %{name}" backup: - attachments_disabled: Attachments may not be included since they exceed the maximum overall size allowed. You can change this via the configuration (requires a server restart). + attachments_disabled: İcazə verilən maksimal cəm sayını aşdığı üçün qoşmalar daxil edilməyə bilər. Bunu konfiqurasiya vasitəsilə dəyişdirə bilərsiniz (serverin yenidən başladılmasını tələb edir). info: > You can trigger a backup here. The process can take some time depending on the amount of data (especially attachments) you have. You will receive an email once it's ready. note: > - A new backup will override any previous one. Only a limited number of backups per day can be requested. - last_backup: Last backup - last_backup_from: Last backup from - title: Backup OpenProject - options: Options - include_attachments: Include attachments - download_backup: Download backup - request_backup: Request backup + Yeni bir nüsxələmə, əvvəlkini etibarsız edəcək. Gündəlik yalnız limitli sayda nüsxələmə tələb oluna bilər. + last_backup: Son nüsxələmə + last_backup_from: Son nüsxələmə + title: OpenProject nüsxələməsi + options: Seçimlər + include_attachments: Qoşmaları daxil et + download_backup: Nüsxəni endir + request_backup: Nüsxə tələb et close_popup_title: "Açılan pəncərəni bağla" - close_filter_title: "Close filter" - close_form_title: "Close form" - button_add_watcher: "Add watcher" - button_add: "Add" - button_back: "Back" - button_back_to_list_view: "Back to list view" - button_cancel: "Cancel" - button_close: "Close" - button_change_project: "Change project" - button_check_all: "Check all" - button_configure-form: "Configure form" - button_confirm: "Confirm" - button_continue: "Continue" - button_copy: "Copy" - button_copy_to_other_project: "Copy to other project" - button_custom-fields: "Custom fields" - button_delete: "Delete" + close_filter_title: "Filtr seç" + close_form_title: "Formu bağla" + button_add_watcher: "Nəzarətçi əlavə et" + button_add: "Əlavə et" + button_back: "Geri" + button_back_to_list_view: "Siyahı görünüşünə qayıt" + button_cancel: "İmtina" + button_close: "Bağla" + button_change_project: "Layihəni dəyişdir" + button_check_all: "Hamısını seç" + button_configure-form: "Formu konfiqurasiya et" + button_confirm: "Təsdiqlə" + button_continue: "Davam" + button_copy: "Kopyala" + button_copy_to_other_project: "Digər layihəyə kopyala" + button_custom-fields: "Özəl sahələr" + button_delete: "Sil" button_delete_watcher: "İzləyicini sil" button_details_view: "Təfsilat baxışı" - button_duplicate: "Duplicate" - button_edit: "Edit" - button_filter: "Filter" - button_collapse_all: "Collapse all" - button_expand_all: "Expand all" - button_advanced_filter: "Advanced filter" + button_duplicate: "Çoxalt" + button_edit: "Düzəliş et" + button_filter: "Filtr" + button_collapse_all: "Hamısını yığcamlaşdır" + button_expand_all: "Hamısını genişləndir" + button_advanced_filter: "Qabaqcıl filtr" button_list_view: "Siyahı baxışı" - button_show_view: "Fullscreen view" - button_log_time: "Log time" - button_more: "More" + button_show_view: "Tam ekran görünüşü" + button_log_time: "Jurnal vaxtı" + button_more: "Daha çox" button_open_details: "Təfsilat baxışını aç" - button_close_details: "Close details view" - button_open_fullscreen: "Open fullscreen view" - button_show_cards: "Show card view" - button_show_list: "Show list view" - button_quote: "Quote" - button_save: "Save" + button_close_details: "Təfsilatlar görünüşünü bağla" + button_open_fullscreen: "Tam ekran görünüşünü aç" + button_show_cards: "Kart görünüşünü göstər" + button_show_list: "Siyahı görünüşünü göstər" + button_quote: "Sitat" + button_save: "Saxla" button_settings: "Settings" button_uncheck_all: "Uncheck all" - button_update: "Update" + button_update: "Yeniləmə" button_export-pdf: "Download PDF" button_export-atom: "Download Atom" button_create: "Create" @@ -147,7 +147,7 @@ az: code_block: button: 'Insert code snippet' title: 'Insert / edit Code snippet' - language: 'Formatting language' + language: 'Formatlama dili' language_hint: 'Enter the formatting language that will be used for highlighting (if supported).' dropdown: macros: 'Macros' From 70f097b94d004fff9386ed489e57b11ed96ff3c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 23 May 2022 08:26:23 +0200 Subject: [PATCH 65/80] Update README.md --- docs/system-admin-guide/authentication/saml/README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/system-admin-guide/authentication/saml/README.md b/docs/system-admin-guide/authentication/saml/README.md index 7d841848c6..1a96dbe08b 100644 --- a/docs/system-admin-guide/authentication/saml/README.md +++ b/docs/system-admin-guide/authentication/saml/README.md @@ -13,7 +13,16 @@ keywords: SAML, SSO, single sign-on, authentication
    You can integrate your active directory or other SAML compliant identity provider in your OpenProject Enterprise Edition. +### Prerequisites +In order to use integrate OpenProject as a service provider (SP) using SAML, your identity providers (idP): + +- needs to be able to handle SAML 2.0 redirect Single-Sign On (SSO) flows, in some implementations also referred to as WebSSO +- has a known or configurable set of attributes that map to the following required OpenProject attributes. The way these attribute mappings will be defined is described later in this document. + - **login**: A stable attribute used to uniquely identify the user. This willl most commonly map to an account ID, samAccountName or email (but please note that emails are often interchangeable, and this might result in logins changing in OpenProject). + - **email**: The email attribute of the user being authenticated + - **first name** and **last name** of the user. +- provides the public certificate or certificate fingerprint (SHA1) in use for communicating with the idP. ### 1: Configuring the SAML integration @@ -68,7 +77,7 @@ saml: # Either `idp_cert` or `idp_cert_fingerprint` must be present! idp_cert_fingerprint: "E7:91:B2:E1:..." - # Replace with your redirect flow single sign on URL + # Replace with your SAML 2.0 redirect flow single sign on URL # For example: "https://sso.example.com/saml/singleSignOn" idp_sso_target_url: "" # Replace with your redirect flow single sign out URL From d04478b4d3634517f4b0fe7a20ea5bbbabe8fc7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 19 May 2022 16:14:12 +0200 Subject: [PATCH 66/80] Provide a way to disable user status syncing in LDAP user sync https://community.openproject.org/work_packages/42485 --- app/services/ldap/base_service.rb | 38 +++++++++++++++---- config/constants/settings/definitions.rb | 6 +++ .../configuration/environment/README.md | 4 +- .../ldap-authentication/README.md | 22 +++++++++++ ...chronize_users_service_integration_spec.rb | 25 ++++++++++++ 5 files changed, 86 insertions(+), 9 deletions(-) diff --git a/app/services/ldap/base_service.rb b/app/services/ldap/base_service.rb index 4539f5afdc..ae3918a631 100644 --- a/app/services/ldap/base_service.rb +++ b/app/services/ldap/base_service.rb @@ -18,15 +18,13 @@ module Ldap raise NotImplementedError end - # rubocop:disable Metrics/AbcSize + protected + 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 + lock_user!(user) if update_attributes.nil? && user.persisted? return unless update_attributes if user.new_record? @@ -35,7 +33,6 @@ module Ldap 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) @@ -44,8 +41,7 @@ module Ldap .call(attrs) if call.success? - # Ensure the user is activated - call.result.update_column(:status, Principal.statuses[:active]) + activate_user!(user) 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}" } @@ -64,6 +60,32 @@ module Ldap end end + ## + # Locks the given user if this is what the sync service should do. + def lock_user!(user) + if OpenProject::Configuration.ldap_users_sync_status? + Rails.logger.info { "Could not find user #{user.login} in #{ldap.name}. Locking the user." } + user.update_column(:status, Principal.statuses[:locked]) + else + Rails.logger.info do + "Could not find user #{user.login} in #{ldap.name}. Ignoring due to ldap_users_sync_status being unset" + end + end + end + + ## + # Activates the given user if this is what the sync service should do. + def activate_user!(user) + if OpenProject::Configuration.ldap_users_sync_status? + Rails.logger.info { "Activating #{user.login} due to it being synced from LDAP #{ldap.name}." } + user.update_column(:status, Principal.statuses[:active]) + else + Rails.logger.info do + "Would activate #{user.login} through #{ldap.name} but ignoring due to ldap_users_sync_status being unset." + end + end + end + ## # Get the user attributes of a single matching LDAP entry. # diff --git a/config/constants/settings/definitions.rb b/config/constants/settings/definitions.rb index 1065ffe7cb..999eb18e76 100644 --- a/config/constants/settings/definitions.rb +++ b/config/constants/settings/definitions.rb @@ -526,6 +526,12 @@ Settings::Definition.define do value: false, writable: false + # Update users' status through the synchronization job + add :ldap_users_sync_status, + format: :boolean, + value: true, + writable: false + add :ldap_tls_options, value: {}, writable: false diff --git a/docs/installation-and-operations/configuration/environment/README.md b/docs/installation-and-operations/configuration/environment/README.md index 48bd898334..f012912622 100644 --- a/docs/installation-and-operations/configuration/environment/README.md +++ b/docs/installation-and-operations/configuration/environment/README.md @@ -135,5 +135,7 @@ OPENPROJECT_SECURITY__BADGE__URL (default="https://releases.openproject.com/v1/c OPENPROJECT_MIGRATION__CHECK__ON__EXCEPTIONS (default=true) OPENPROJECT_SHOW__PENDING__MIGRATIONS__WARNING (default=true) OPENPROJECT_SHOW__WARNING__BARS (default=true) -OPENPROJECT_SHOW__STORAGE__INFORMATION (default=true) +OPENPROJECT_SHOW__STORAGE__INFORMATION (default=true) +OPENPROJECT_LDAP__USERS__SYNC__STATUS (default=true) +OPENPROJECT_LDAP__USERS__DISABLE__SYNC__JOB(default=false) ``` diff --git a/docs/system-admin-guide/authentication/ldap-authentication/README.md b/docs/system-admin-guide/authentication/ldap-authentication/README.md index 14961a7322..625a5a2d17 100644 --- a/docs/system-admin-guide/authentication/ldap-authentication/README.md +++ b/docs/system-admin-guide/authentication/ldap-authentication/README.md @@ -137,3 +137,25 @@ With the [OpenProject Enterprise Edition](https://www.openproject.org/enterprise OpenProject supports multiple LDAP connections to source users from. The user's authentication source is remembered the first time it is created (but can be switched in the administration backend). This ensures that the correct connection / LDAP source will be used for the user. Duplicates in the unique attributes (login, email) are not allowed and a second user with the same attributes will not be able to login. Please ensure that amongst all LDAP connections, a unique attribute is used that does not result in conflicting logins. + + + +## LDAP user synchronization + +By default, OpenProject will synchronize user account details (name, e-mail, login) and their account status from the LDAP through a background worker job every 24 hours. + +The user will be ensured to be active if it can be found in LDAP. Likewise, if the user cannot be found in the LDAP, its associated OpenProject account will be locked. + +### **Disabling status synchronization** + +If you wish to synchronize account data from the LDAP, but not synchronize the status to the associated OpenProject account, you can do so with the following configuration variable: + +- `ldap_users_sync_status: false` +- (or the ENV variable `OPENPROJECT_LDAP__USERS__SYNC__STATUS=false`) + +### Disabling the synchronization job + +If for any reason, you do not wish to perform the synchronization at all, you can also remove the synchronization job from being run at all with the following variable: + +- `ldap_users_disable_sync_job: true` +- (or the ENV variable `OPENPROJECT_LDAP__USERS__DISABLE__SYNC__JOB=true`) diff --git a/spec/services/ldap/synchronize_users_service_integration_spec.rb b/spec/services/ldap/synchronize_users_service_integration_spec.rb index 367925d10e..551ad49af3 100644 --- a/spec/services/ldap/synchronize_users_service_integration_spec.rb +++ b/spec/services/ldap/synchronize_users_service_integration_spec.rb @@ -72,6 +72,20 @@ describe Ldap::SynchronizeUsersService do expect(user_aa729).to be_active end + context 'when requesting not to sync users status', + with_config: { ldap_users_sync_status: false } do + it 'does not reactivate the account if it is locked' do + user_aa729.lock! + + expect(user_aa729.reload).to be_locked + + subject + + expect(user_aa729.reload).to be_locked + expect(user_aa729).not_to be_active + end + end + context 'when requesting only a subset of users' do let!(:user_cc414) { create :user, login: 'cc414', auth_source: auth_source } @@ -110,6 +124,17 @@ describe Ldap::SynchronizeUsersService do expect(user_foo.reload).to be_locked end + + context 'when requesting not to sync users status', + with_config: { ldap_users_sync_status: false } do + it 'does not lock that user' do + expect(user_foo).to be_active + + subject + + expect(user_foo.reload).to be_active + end + end end context 'with a user that is in another LDAP' do From 8e4fa4998bec653bd721dcd2d8ccb94c283cd32a Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 23 May 2022 10:32:36 +0200 Subject: [PATCH 67/80] bump airbrake --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index dd5cc2dbe2..e1581bbfa8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -278,7 +278,7 @@ GEM public_suffix (>= 2.0.2, < 5.0) aes_key_wrap (1.1.0) afm (0.2.2) - airbrake (13.0.1) + airbrake (13.0.2) airbrake-ruby (~> 6.0) airbrake-ruby (6.1.0) rbtree3 (~> 0.5) From 8677dec810527c81f4ad56ff9fe000c460da29e8 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 23 May 2022 10:32:55 +0200 Subject: [PATCH 68/80] bump aws-partitions --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e1581bbfa8..30bce3ffc7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -289,7 +289,7 @@ GEM awesome_nested_set (3.5.0) activerecord (>= 4.0.0, < 7.1) aws-eventstream (1.2.0) - aws-partitions (1.587.0) + aws-partitions (1.589.0) aws-sdk-core (3.130.2) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) From 4fcb0312c2c636c7993f714f723b28d843e433ba Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 23 May 2022 10:33:14 +0200 Subject: [PATCH 69/80] bump aws-sdk-core --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 30bce3ffc7..c83025c14b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -290,11 +290,11 @@ GEM activerecord (>= 4.0.0, < 7.1) aws-eventstream (1.2.0) aws-partitions (1.589.0) - aws-sdk-core (3.130.2) + aws-sdk-core (3.131.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) - jmespath (~> 1.0) + jmespath (~> 1, >= 1.6.1) aws-sdk-kms (1.56.0) aws-sdk-core (~> 3, >= 3.127.0) aws-sigv4 (~> 1.1) From aa674f9617a8c94c123df408af78774e2090d83f Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 23 May 2022 10:33:34 +0200 Subject: [PATCH 70/80] bump aws-sdk-kms --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index c83025c14b..d6a07f023d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -295,7 +295,7 @@ GEM aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.56.0) + aws-sdk-kms (1.57.0) aws-sdk-core (~> 3, >= 3.127.0) aws-sigv4 (~> 1.1) aws-sdk-s3 (1.114.0) From 08d237b504b865358a0319bfe420cdfc4b6b8e57 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 23 May 2022 10:37:07 +0200 Subject: [PATCH 71/80] bump oj --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d6a07f023d..538b1c23e4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -663,7 +663,7 @@ GEM octokit (4.22.0) faraday (>= 0.9) sawyer (~> 0.8.0, >= 0.5.3) - oj (3.13.11) + oj (3.13.13) okcomputer (1.18.4) omniauth-saml (1.10.3) omniauth (~> 1.3, >= 1.3.2) From eee61a7244c076224f1ba2b9091fee7d86283420 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 23 May 2022 10:37:50 +0200 Subject: [PATCH 72/80] bump parallel_tests --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 538b1c23e4..0741dad539 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -682,7 +682,7 @@ GEM openproject-token (2.2.0) activemodel parallel (1.22.1) - parallel_tests (3.8.1) + parallel_tests (3.9.0) parallel parser (3.1.2.0) ast (~> 2.4.1) From 45eb49f00ee5fab2dc5b88d8a0b489ca13883d84 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 23 May 2022 10:38:31 +0200 Subject: [PATCH 73/80] bump rubocop-rspec --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0741dad539..92f5428112 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -869,7 +869,7 @@ GEM activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.7.0, < 2.0) - rubocop-rspec (2.10.0) + rubocop-rspec (2.11.1) rubocop (~> 1.19) ruby-duration (3.2.3) activesupport (>= 3.0.0) From ec051d7e44d8b0af27a0054d27245eaf916f83cc Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 23 May 2022 10:45:26 +0200 Subject: [PATCH 74/80] lock sprockets --- Gemfile | 2 ++ Gemfile.lock | 1 + 2 files changed, 3 insertions(+) diff --git a/Gemfile b/Gemfile index fcc2bd589a..fa3d341fc4 100644 --- a/Gemfile +++ b/Gemfile @@ -167,6 +167,8 @@ end gem 'i18n-js', '~> 3.9.0' gem 'rails-i18n', '~> 7.0.0' + +gem 'sprockets', '~> 3.7.2' # lock sprockets below 4.0 gem 'sprockets-rails', '~> 3.4.2' gem 'puma', '~> 5.6' diff --git a/Gemfile.lock b/Gemfile.lock index 92f5428112..e7adf36d72 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1160,6 +1160,7 @@ DEPENDENCIES shoulda-matchers (~> 5.0) spring spring-commands-rspec + sprockets (~> 3.7.2) sprockets-rails (~> 3.4.2) stackprof stringex (~> 2.8.5) From 06334ea33a758c5d1a7dcca6aae4e437d12a6cbe Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 23 May 2022 10:48:39 +0200 Subject: [PATCH 75/80] simplify code --- app/services/authorization/user_allowed_service.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/services/authorization/user_allowed_service.rb b/app/services/authorization/user_allowed_service.rb index 7480028051..4b8138e73c 100644 --- a/app/services/authorization/user_allowed_service.rb +++ b/app/services/authorization/user_allowed_service.rb @@ -93,10 +93,9 @@ class Authorization::UserAllowedService has_authorized_role?(action, project) end + # Authorize if user is authorized on every element of the array def allowed_to_in_all_projects?(action, projects) - projects = projects.to_a - # Authorize if user is authorized on every element of the array - projects.present? && projects.all? do |project| + Array(projects).all? do |project| allowed_to?(action, project) end end @@ -140,7 +139,7 @@ class Authorization::UserAllowedService def normalize_action(action) if action.is_a?(Hash) && action[:controller] && action[:controller].to_s.starts_with?('/') action = action.dup - action[:controller] = action[:controller][1..-1] + action[:controller] = action[:controller][1..] end action From a17991b85c771467f24a28c3f8e64287c76b47bc Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 23 May 2022 11:09:47 +0200 Subject: [PATCH 76/80] handle nil/empty explicitly --- app/services/authorization/user_allowed_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/authorization/user_allowed_service.rb b/app/services/authorization/user_allowed_service.rb index 4b8138e73c..27bc85b547 100644 --- a/app/services/authorization/user_allowed_service.rb +++ b/app/services/authorization/user_allowed_service.rb @@ -95,7 +95,7 @@ class Authorization::UserAllowedService # Authorize if user is authorized on every element of the array def allowed_to_in_all_projects?(action, projects) - Array(projects).all? do |project| + projects.present? && Array(projects).all? do |project| allowed_to?(action, project) end end From bbea68efcaaf4cc1f593b2afec74ac580a2b1dcb Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 23 May 2022 14:15:46 +0200 Subject: [PATCH 77/80] attempt to stabilize spec --- .../details/markdown/activity_comments_spec.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spec/features/work_packages/details/markdown/activity_comments_spec.rb b/spec/features/work_packages/details/markdown/activity_comments_spec.rb index 6ae739f213..9e62db7b7b 100644 --- a/spec/features/work_packages/details/markdown/activity_comments_spec.rb +++ b/spec/features/work_packages/details/markdown/activity_comments_spec.rb @@ -219,14 +219,21 @@ describe 'activity comments', js: true, with_mail: false do end describe 'referencing another work package' do - let!(:work_package2) { create(:work_package, project: project) } + let!(:work_package2) { create(:work_package, project: project, type: create(:type)) } it 'can reference another work package with all methods' do comment_field.activate! + # Insert a new reference using the autocompleter + comment_field.input_element.send_keys "Single ##{work_package2.id}" + expect(page) + .to have_selector('.mention-list-item', text: "#{work_package2.type.name} ##{work_package2.id}:") + + find('.mention-list-item', text: "#{work_package2.type.name} ##{work_package2.id}:").click + # Insert new text, need to do this separately. + # No autocompleter used this time. [ - "Single ##{work_package2.id}", :return, "Double ###{work_package2.id}", :return, From bc8ddc6b1e89e90a4910820f0e3afa6f7381e510 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 23 May 2022 14:33:50 +0200 Subject: [PATCH 78/80] use default key for settings definition --- config/constants/settings/definitions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/constants/settings/definitions.rb b/config/constants/settings/definitions.rb index 81b91ad514..c88a6eb096 100644 --- a/config/constants/settings/definitions.rb +++ b/config/constants/settings/definitions.rb @@ -529,7 +529,7 @@ Settings::Definition.define do # Update users' status through the synchronization job add :ldap_users_sync_status, format: :boolean, - value: true, + default: true, writable: false add :ldap_tls_options, From f295da57e9fe0d958ffdeab50915c295624619be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 23 May 2022 18:41:04 +0200 Subject: [PATCH 79/80] Disable modules in after initialize --- config/initializers/module_handler.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/config/initializers/module_handler.rb b/config/initializers/module_handler.rb index ea1675ca98..6960d30c64 100644 --- a/config/initializers/module_handler.rb +++ b/config/initializers/module_handler.rb @@ -26,7 +26,9 @@ # See COPYRIGHT and LICENSE files for more details. #++ -if OpenProject::Configuration.disabled_modules.any? - to_disable = OpenProject::Configuration.disabled_modules - OpenProject::Plugins::ModuleHandler.disable_modules(to_disable) +Rails.application.config.after_initialize do + if OpenProject::Configuration.disabled_modules.any? + to_disable = OpenProject::Configuration.disabled_modules + OpenProject::Plugins::ModuleHandler.disable_modules(to_disable) + end end From 980c4c81eb730443c13b498cd03ebeaf1fcf41e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 May 2022 05:49:09 +0000 Subject: [PATCH 80/80] Bump parallel_tests from 3.9.0 to 3.10.0 Bumps [parallel_tests](https://github.com/grosser/parallel_tests) from 3.9.0 to 3.10.0. - [Release notes](https://github.com/grosser/parallel_tests/releases) - [Changelog](https://github.com/grosser/parallel_tests/blob/master/CHANGELOG.md) - [Commits](https://github.com/grosser/parallel_tests/compare/v3.9.0...v3.10.0) --- updated-dependencies: - dependency-name: parallel_tests dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e7adf36d72..842743fdc6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -682,7 +682,7 @@ GEM openproject-token (2.2.0) activemodel parallel (1.22.1) - parallel_tests (3.9.0) + parallel_tests (3.10.0) parallel parser (3.1.2.0) ast (~> 2.4.1)