diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index b2eac6ca41e7..364045d10d63 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -5,3 +5,5 @@ 21a696ef9b170e14ad2daf53364a4c2113822c2f # Update copyright information for 2023 c795874f7f281297bbd3bad2fdb58b24cb4ce624 +# switch to double quotes in most changed files with a lot of quotes +5c72ea0046a6b5230bf456f55a296ed6fd579535 diff --git a/Gemfile b/Gemfile index 45910ccfdb70..f7059356a44d 100644 --- a/Gemfile +++ b/Gemfile @@ -26,361 +26,361 @@ # See COPYRIGHT and LICENSE files for more details. #++ -source 'https://rubygems.org' +source "https://rubygems.org" # TODO: Once packager.io and heroku buildpacks support bundler 2.4.22, # then we can use the new bundler syntax `ruby file: '.ruby-version'`. # https://github.com/heroku/heroku-buildpack-ruby/issues/1408#issuecomment-1841596215 -ruby File.read('.ruby-version').strip +ruby File.read(".ruby-version").strip -gem 'actionpack-xml_parser', '~> 2.0.0' -gem 'activemodel-serializers-xml', '~> 1.0.1' -gem 'activerecord-import', '~> 1.5.0' -gem 'activerecord-session_store', '~> 2.1.0' -gem 'ox' -gem 'rails', '~> 7.1.3' -gem 'responders', '~> 3.0' +gem "actionpack-xml_parser", "~> 2.0.0" +gem "activemodel-serializers-xml", "~> 1.0.1" +gem "activerecord-import", "~> 1.5.0" +gem "activerecord-session_store", "~> 2.1.0" +gem "ox" +gem "rails", "~> 7.1.3" +gem "responders", "~> 3.0" -gem 'ffi', '~> 1.15' +gem "ffi", "~> 1.15" -gem 'rdoc', '>= 2.4.2' +gem "rdoc", ">= 2.4.2" -gem 'doorkeeper', '~> 5.6.6' +gem "doorkeeper", "~> 5.6.6" # Maintain our own omniauth due to relative URL root issues # see upstream PR: https://github.com/omniauth/omniauth/pull/903 -gem 'omniauth', git: 'https://github.com/opf/omniauth', ref: 'fe862f986b2e846e291784d2caa3d90a658c67f0' -gem 'request_store', '~> 1.6.0' +gem "omniauth", git: "https://github.com/opf/omniauth", ref: "fe862f986b2e846e291784d2caa3d90a658c67f0" +gem "request_store", "~> 1.6.0" -gem 'warden', '~> 1.2' -gem 'warden-basic_auth', '~> 0.2.1' +gem "warden", "~> 1.2" +gem "warden-basic_auth", "~> 0.2.1" -gem 'will_paginate', '~> 4.0.0' +gem "will_paginate", "~> 4.0.0" -gem 'friendly_id', '~> 5.5.0' +gem "friendly_id", "~> 5.5.0" -gem 'acts_as_list', '~> 1.1.0' -gem 'acts_as_tree', '~> 2.9.0' -gem 'awesome_nested_set', '~> 3.6.0' -gem 'closure_tree', '~> 7.4.0' -gem 'rubytree', '~> 2.0.0' +gem "acts_as_list", "~> 1.1.0" +gem "acts_as_tree", "~> 2.9.0" +gem "awesome_nested_set", "~> 3.6.0" +gem "closure_tree", "~> 7.4.0" +gem "rubytree", "~> 2.0.0" # Only used in down migrations now. # Is to be removed once the referencing migrations have been squashed. -gem 'typed_dag', '~> 2.0.2', require: false +gem "typed_dag", "~> 2.0.2", require: false -gem 'addressable', '~> 2.8.0' +gem "addressable", "~> 2.8.0" # Remove whitespace from model input -gem 'auto_strip_attributes', '~> 2.5' +gem "auto_strip_attributes", "~> 2.5" # Provide timezone info for TZInfo used by AR -gem 'tzinfo-data', '~> 1.2024.1' +gem "tzinfo-data", "~> 1.2024.1" # to generate html-diffs (e.g. for wiki comparison) -gem 'htmldiff' +gem "htmldiff" # Generate url slugs with #to_url and other string niceties -gem 'stringex', '~> 2.8.5' +gem "stringex", "~> 2.8.5" # CommonMark markdown parser with GFM extension -gem 'commonmarker', '~> 1.0.3' +gem "commonmarker", "~> 1.0.3" # HTML pipeline for transformations on text formatter output # such as sanitization or additional features -gem 'html-pipeline', '~> 2.14.0' +gem "html-pipeline", "~> 2.14.0" # Tasklist parsing and renderer -gem 'deckar01-task_list', '~> 2.3.1' +gem "deckar01-task_list", "~> 2.3.1" # Requires escape-utils for faster escaping -gem 'escape_utils', '~> 1.3' +gem "escape_utils", "~> 1.3" # Syntax highlighting used in html-pipeline with rouge -gem 'rouge', '~> 4.2.0' +gem "rouge", "~> 4.2.0" # HTML sanitization used for html-pipeline -gem 'sanitize', '~> 6.1.0' +gem "sanitize", "~> 6.1.0" # HTML autolinking for mails and urls (replaces autolink) -gem 'rinku', '~> 2.0.4', require: %w[rinku rails_rinku] +gem "rinku", "~> 2.0.4", require: %w[rinku rails_rinku] # Version parsing with semver -gem 'semantic', '~> 1.6.1' +gem "semantic", "~> 1.6.1" # generates SVG Graphs # used for statistics on svn repositories -gem 'svg-graph', '~> 2.2.0' +gem "svg-graph", "~> 2.2.0" -gem 'date_validator', '~> 0.12.0' -gem 'email_validator', '~> 2.2.3' -gem 'json_schemer', '~> 2.2.0' -gem 'ruby-duration', '~> 3.2.0' +gem "date_validator", "~> 0.12.0" +gem "email_validator", "~> 2.2.3" +gem "json_schemer", "~> 2.2.0" +gem "ruby-duration", "~> 3.2.0" # `config/initializers/mail_starttls_patch.rb` has also been patched to # fix STARTTLS handling until https://github.com/mikel/mail/pull/1536 is # released. -gem 'mail', '= 2.8.1' +gem "mail", "= 2.8.1" # provide compatible filesystem information for available storage -gem 'sys-filesystem', '~> 1.4.0', require: false +gem "sys-filesystem", "~> 1.4.0", require: false -gem 'bcrypt', '~> 3.1.6' +gem "bcrypt", "~> 3.1.6" -gem 'multi_json', '~> 1.15.0' -gem 'oj', '~> 3.16.0' +gem "multi_json", "~> 1.15.0" +gem "oj", "~> 3.16.0" -gem 'daemons' -gem 'good_job', '~> 3.26.1' # update should be done manually in sync with saas-openproject version. +gem "daemons" +gem "good_job", "~> 3.26.1" # update should be done manually in sync with saas-openproject version. -gem 'rack-protection', '~> 3.2.0' +gem "rack-protection", "~> 3.2.0" # Rack::Attack is a rack middleware to protect your web app from bad clients. # It allows whitelisting, blacklisting, throttling, and tracking based # on arbitrary properties of the request. # https://github.com/kickstarter/rack-attack -gem 'rack-attack', '~> 6.7.0' +gem "rack-attack", "~> 6.7.0" # CSP headers -gem 'secure_headers', '~> 6.5.0' +gem "secure_headers", "~> 6.5.0" # Browser detection for incompatibility checks -gem 'browser', '~> 5.3.0' +gem "browser", "~> 5.3.0" # Providing health checks -gem 'okcomputer', '~> 1.18.1' +gem "okcomputer", "~> 1.18.1" -gem 'gon', '~> 6.4.0' +gem "gon", "~> 6.4.0" # Lograge to provide sane and non-verbose logging -gem 'lograge', '~> 0.14.0' +gem "lograge", "~> 0.14.0" # Structured warnings to selectively disable them in production -gem 'structured_warnings', '~> 0.4.0' +gem "structured_warnings", "~> 0.4.0" # catch exceptions and send them to any airbrake compatible backend # don't require by default, instead load on-demand when actually configured -gem 'airbrake', '~> 13.0.0', require: false +gem "airbrake", "~> 13.0.0", require: false -gem 'md_to_pdf', git: 'https://github.com/opf/md-to-pdf', ref: '8f14736a88ad0064d2a97be108fe7061ffbcee91' -gem 'prawn', '~> 2.4' +gem "md_to_pdf", git: "https://github.com/opf/md-to-pdf", ref: "8f14736a88ad0064d2a97be108fe7061ffbcee91" +gem "prawn", "~> 2.4" # prawn implicitly depends on matrix gem no longer in ruby core with 3.1 -gem 'matrix', '~> 0.4.2' +gem "matrix", "~> 0.4.2" -gem 'meta-tags', '~> 2.20.0' +gem "meta-tags", "~> 2.20.0" -gem 'paper_trail', '~> 15.1.0' +gem "paper_trail", "~> 15.1.0" -gem 'clamav-client', github: 'honestica/clamav-client', ref: '29e78ae94307cb34e79ddd29c5da79752239d8b7' +gem "clamav-client", github: "honestica/clamav-client", ref: "29e78ae94307cb34e79ddd29c5da79752239d8b7" group :production do # we use dalli as standard memcache client # requires memcached 1.4+ - gem 'dalli', '~> 3.2.0' - gem 'redis', '~> 5.1.0' + gem "dalli", "~> 3.2.0" + gem "redis", "~> 5.1.0" end -gem 'i18n-js', '~> 4.2.3' -gem 'rails-i18n', '~> 7.0.0' +gem "i18n-js", "~> 4.2.3" +gem "rails-i18n", "~> 7.0.0" -gem 'sprockets', '~> 3.7.2' # lock sprockets below 4.0 -gem 'sprockets-rails', '~> 3.4.2' +gem "sprockets", "~> 3.7.2" # lock sprockets below 4.0 +gem "sprockets-rails", "~> 3.4.2" -gem 'puma', '~> 6.4' -gem 'puma-plugin-statsd', '~> 2.0' -gem 'rack-timeout', '~> 0.6.3', require: 'rack/timeout/base' +gem "puma", "~> 6.4" +gem "puma-plugin-statsd", "~> 2.0" +gem "rack-timeout", "~> 0.6.3", require: "rack/timeout/base" -gem 'nokogiri', '~> 1.16.0' +gem "nokogiri", "~> 1.16.0" -gem 'carrierwave', '~> 1.3.1' -gem 'carrierwave_direct', '~> 2.1.0' -gem 'fog-aws' +gem "carrierwave", "~> 1.3.1" +gem "carrierwave_direct", "~> 2.1.0" +gem "fog-aws" -gem 'aws-sdk-core', '~> 3.107' +gem "aws-sdk-core", "~> 3.107" # File upload via fog + screenshots on travis -gem 'aws-sdk-s3', '~> 1.91' +gem "aws-sdk-s3", "~> 1.91" -gem 'openproject-token', '~> 4.0' +gem "openproject-token", "~> 4.0" -gem 'plaintext', '~> 0.3.2' +gem "plaintext", "~> 0.3.2" -gem 'ruby-progressbar', '~> 1.13.0', require: false +gem "ruby-progressbar", "~> 1.13.0", require: false -gem 'mini_magick', '~> 4.12.0', require: false +gem "mini_magick", "~> 4.12.0", require: false -gem 'validate_url' +gem "validate_url" # Storages support code -gem 'dry-container' +gem "dry-container" # ActiveRecord extension which adds typecasting to store accessors -gem 'store_attribute', '~> 1.0' +gem "store_attribute", "~> 1.0" # Appsignal integration -gem 'appsignal', '~> 3.0', require: false +gem "appsignal", "~> 3.0", require: false -gem 'view_component' +gem "view_component" # Lookbook -gem 'lookbook', '~> 2.2.1' +gem "lookbook", "~> 2.2.1" # Require factory_bot for usage with openproject plugins testing -gem 'factory_bot', '~> 6.4.0', require: false +gem "factory_bot", "~> 6.4.0", require: false # require factory_bot_rails for convenience in core development -gem 'factory_bot_rails', '~> 6.4.0', require: false +gem "factory_bot_rails", "~> 6.4.0", require: false -gem 'turbo-rails', '~> 2.0.0' +gem "turbo-rails", "~> 2.0.0" -gem 'httpx' +gem "httpx" group :test do - gem 'launchy', '~> 3.0.0' - gem 'rack-test', '~> 2.1.0' - gem 'shoulda-context', '~> 2.0' + gem "launchy", "~> 3.0.0" + gem "rack-test", "~> 2.1.0" + gem "shoulda-context", "~> 2.0" # Test prof provides factories from code # and other niceties - gem 'test-prof', '~> 1.3.0' - gem 'turbo_tests', github: 'crohr/turbo_tests', ref: 'fix/runtime-info' + gem "test-prof", "~> 1.3.0" + gem "turbo_tests", github: "crohr/turbo_tests", ref: "fix/runtime-info" - gem 'rack_session_access' - gem 'rspec', '~> 3.13.0' + gem "rack_session_access" + gem "rspec", "~> 3.13.0" # also add to development group, so 'spec' rake task gets loaded - gem 'rspec-rails', '~> 6.1.0', group: :development + gem "rspec-rails", "~> 6.1.0", group: :development # Retry failures within the same environment - gem 'retriable', '~> 3.1.1' - gem 'rspec-retry', '~> 0.6.1' + gem "retriable", "~> 3.1.1" + gem "rspec-retry", "~> 0.6.1" # Accessibility tests - gem 'axe-core-rspec' + gem "axe-core-rspec" # Modify ENV - gem 'climate_control' + gem "climate_control" # XML comparison tests - gem 'compare-xml', '~> 0.66', require: false + gem "compare-xml", "~> 0.66", require: false # PDF Export tests - gem 'pdf-inspector', '~> 1.2' + gem "pdf-inspector", "~> 1.2" # brings back testing for 'assigns' and 'assert_template' extracted in rails 5 - gem 'rails-controller-testing', '~> 1.0.2' + gem "rails-controller-testing", "~> 1.0.2" - gem 'capybara', '~> 3.40.0' - gem 'capybara_accessible_selectors', git: 'https://github.com/citizensadvice/capybara_accessible_selectors', branch: 'main' - gem 'capybara-screenshot', '~> 1.0.17' - gem 'cuprite', '~> 0.15.0' - gem 'selenium-devtools' - gem 'selenium-webdriver', '~> 4.18.0' + gem "capybara", "~> 3.40.0" + gem "capybara_accessible_selectors", git: "https://github.com/citizensadvice/capybara_accessible_selectors", branch: "main" + gem "capybara-screenshot", "~> 1.0.17" + gem "cuprite", "~> 0.15.0" + gem "selenium-devtools" + gem "selenium-webdriver", "~> 4.18.0" - gem 'fuubar', '~> 2.5.0' - gem 'timecop', '~> 0.9.0' + gem "fuubar", "~> 2.5.0" + gem "timecop", "~> 0.9.0" # Record your test suite's HTTP interactions and replay them during future test runs for fast, deterministic, accurate tests. - gem 'vcr' + gem "vcr" # Mock backend requests (for ruby tests) - gem 'webmock', '~> 3.12', require: false + gem "webmock", "~> 3.12", require: false # Mock selenium requests through proxy (for feature tests) - gem 'puffing-billy', '~> 4.0.0' - gem 'table_print', '~> 1.5.6' + gem "puffing-billy", "~> 4.0.0" + gem "table_print", "~> 1.5.6" - gem 'equivalent-xml', '~> 0.6' - gem 'json_spec', '~> 1.1.4' - gem 'shoulda-matchers', '~> 6.0', require: nil + gem "equivalent-xml", "~> 0.6" + gem "json_spec", "~> 1.1.4" + gem "shoulda-matchers", "~> 6.0", require: nil - gem 'parallel_tests', '~> 4.0' + gem "parallel_tests", "~> 4.0" end group :ldap do - gem 'net-ldap', '~> 0.19.0' + gem "net-ldap", "~> 0.19.0" end group :development do - gem 'listen', '~> 3.9.0' # Use for event-based reloaders + gem "listen", "~> 3.9.0" # Use for event-based reloaders - gem 'letter_opener' + gem "letter_opener" - gem 'spring' - gem 'spring-commands-rspec' - gem 'spring-commands-rubocop' + gem "spring" + gem "spring-commands-rspec" + gem "spring-commands-rubocop" - gem 'colored2' + gem "colored2" # git hooks manager - gem 'lefthook', require: false + gem "lefthook", require: false end group :development, :test do - gem 'dotenv-rails' + gem "dotenv-rails" # Tracing and profiling gems - gem 'flamegraph', require: false - gem 'rack-mini-profiler', require: false - gem 'ruby-prof', require: false - gem 'stackprof', require: false + gem "flamegraph", require: false + gem "rack-mini-profiler", require: false + gem "ruby-prof", require: false + gem "stackprof", require: false # REPL with debug commands - gem 'debug' + gem "debug" - gem 'pry-byebug', '~> 3.10.0', platforms: [:mri] - gem 'pry-doc' - gem 'pry-rails', '~> 0.3.6' - gem 'pry-rescue', '~> 1.6.0' + gem "pry-byebug", "~> 3.10.0", platforms: [:mri] + gem "pry-doc" + gem "pry-rails", "~> 0.3.6" + gem "pry-rescue", "~> 1.6.0" # ruby linting - gem 'rubocop', require: false - gem 'rubocop-inflector', require: false - gem 'rubocop-performance', require: false - gem 'rubocop-rails', require: false - gem 'rubocop-rspec', require: false + gem "rubocop", require: false + gem "rubocop-inflector", require: false + gem "rubocop-performance", require: false + gem "rubocop-rails", require: false + gem "rubocop-rspec", require: false # erb linting - gem 'erb_lint', require: false - gem 'erblint-github', require: false + gem "erb_lint", require: false + gem "erblint-github", require: false # Brakeman scanner - gem 'brakeman', '~> 6.1.0' + gem "brakeman", "~> 6.1.0" # i18n-tasks helps find and manage missing and unused translations. - gem 'i18n-tasks', '~> 1.0.13' + gem "i18n-tasks", "~> 1.0.13" end -gem 'bootsnap', '~> 1.18.0', require: false +gem "bootsnap", "~> 1.18.0", require: false # API gems -gem 'grape', '~> 2.0.0' -gem 'grape_logging', '~> 1.8.4' -gem 'roar', '~> 1.2.0' +gem "grape", "~> 2.0.0" +gem "grape_logging", "~> 1.8.4" +gem "roar", "~> 1.2.0" # CORS for API -gem 'rack-cors', '~> 2.0.2' +gem "rack-cors", "~> 2.0.2" # Gmail API -gem 'google-apis-gmail_v1', require: false -gem 'googleauth', require: false +gem "google-apis-gmail_v1", require: false +gem "googleauth", require: false # Required for contracts -gem 'disposable', '~> 0.6.2' +gem "disposable", "~> 0.6.2" platforms :mri, :mingw, :x64_mingw do group :postgres do - gem 'pg', '~> 1.5.0' + gem "pg", "~> 1.5.0" end # Support application loading when no database exists yet. - gem 'activerecord-nulldb-adapter', '~> 1.0.0' + gem "activerecord-nulldb-adapter", "~> 1.0.0" # Have application level locks on the database to have a mutex shared between workers/hosts. # We e.g. employ this to safeguard the creation of journals. - gem 'with_advisory_lock', '~> 5.1.0' + gem "with_advisory_lock", "~> 5.1.0" end # Load Gemfile.modules explicitly to allow dependabot to work -eval_gemfile './Gemfile.modules' +eval_gemfile "./Gemfile.modules" # Load Gemfile.local, Gemfile.plugins and custom Gemfiles -gemfiles = Dir.glob File.expand_path('{Gemfile.plugins,Gemfile.local}', __dir__) -gemfiles << ENV['CUSTOM_PLUGIN_GEMFILE'] unless ENV['CUSTOM_PLUGIN_GEMFILE'].nil? +gemfiles = Dir.glob File.expand_path("{Gemfile.plugins,Gemfile.local}", __dir__) +gemfiles << ENV["CUSTOM_PLUGIN_GEMFILE"] unless ENV["CUSTOM_PLUGIN_GEMFILE"].nil? gemfiles.each do |file| # We use send to allow dependabot to function # don't use eval_gemfile(file) here as it will break dependabot! send(:eval_gemfile, file) if File.readable?(file) end -gem 'openproject-octicons', '~>19.8.0' -gem 'openproject-octicons_helper', '~>19.8.0' -gem 'openproject-primer_view_components', '~>0.23.0' +gem "openproject-octicons", "~>19.8.0" +gem "openproject-octicons_helper", "~>19.8.0" +gem "openproject-primer_view_components", "~>0.23.0" diff --git a/app/components/open_project/common/attribute_component.rb b/app/components/open_project/common/attribute_component.rb index 12d1584a7b6e..e77a2f9892b5 100644 --- a/app/components/open_project/common/attribute_component.rb +++ b/app/components/open_project/common/attribute_component.rb @@ -57,7 +57,7 @@ def full_text end def display_expand_button_value - multi_type? || text_ast.xpath('html/body').children.length > 1 ? :block : :none + multi_type? || body_children.length > 1 ? :block : :none end def text_color @@ -67,18 +67,26 @@ def text_color private def first_paragraph - @first_paragraph ||= text_ast - .xpath('html/body') - .children - .first - .inner_html - .html_safe # rubocop:disable Rails/OutputSafety + @first_paragraph ||= if body_children.any? + body_children + .first + .inner_html + .html_safe # rubocop:disable Rails/OutputSafety + else + '' + end end def text_ast @text_ast ||= Nokogiri::HTML(full_text) end + def body_children + text_ast + .xpath('html/body') + .children + end + def multi_type? first_paragraph.include?('figure') || first_paragraph.include?('macro') end diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 91dd292afc99..d670f34df98c 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -57,7 +57,7 @@ def projects end def plugins - @plugins = Redmine::Plugin.all.sort + @plugins = Redmine::Plugin.not_bundled.sort end def test_email diff --git a/app/views/admin/plugins.html.erb b/app/views/admin/plugins.html.erb index 2343204c7d44..5ad7fe0851f0 100644 --- a/app/views/admin/plugins.html.erb +++ b/app/views/admin/plugins.html.erb @@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <% html_title t(:label_administration), t(:label_plugins) %> -<%= toolbar title: t(:label_modules_and_plugins) %> +<%= toolbar title: t(:label_plugins) %> <% if @plugins.any? %>
@@ -88,5 +88,6 @@ See COPYRIGHT and LICENSE files for more details.
<% else %> - <%= no_results_box %> + <%= no_results_box display_action: true, + action_url: OpenProject::Static::Links.url_for(:integrations) %> <% end %> diff --git a/config/constants/settings/definition.rb b/config/constants/settings/definition.rb index b71ba78e7a4b..c3ec3526f229 100644 --- a/config/constants/settings/definition.rb +++ b/config/constants/settings/definition.rb @@ -28,7 +28,7 @@ module Settings class Definition - ENV_PREFIX = 'OPENPROJECT_'.freeze + ENV_PREFIX = "OPENPROJECT_".freeze AR_BOOLEAN_TYPE = ActiveRecord::Type::Boolean.new DEFINITIONS = { activity_days_default: { @@ -36,34 +36,34 @@ class Definition }, after_first_login_redirect_url: { format: :string, - description: 'URL users logging in for the first time will be redirected to (e.g., a help screen)', + description: "URL users logging in for the first time will be redirected to (e.g., a help screen)", default: nil }, after_login_default_redirect_url: { - description: 'Override URL to which logged in users are redirected instead of the My page', + description: "Override URL to which logged in users are redirected instead of the My page", format: :string, default: nil }, apiv3_cors_enabled: { - description: 'Enable CORS headers for APIv3 server responses', + description: "Enable CORS headers for APIv3 server responses", default: false }, apiv3_cors_origins: { default: [] }, apiv3_docs_enabled: { - description: 'Enable interactive APIv3 documentation as part of the application', + description: "Enable interactive APIv3 documentation as part of the application", default: true }, apiv3_enable_basic_auth: { - description: 'Enable API token or global basic authentication for APIv3 requests', + description: "Enable API token or global basic authentication for APIv3 requests", default: true }, apiv3_max_page_size: { default: 1000 }, app_title: { - default: 'OpenProject' + default: "OpenProject" }, attachment_max_size: { default: 5120 @@ -76,41 +76,41 @@ class Definition # Carrierwave storage type. Possible values are, among others, :file and :fog. # The latter requires further configuration. attachments_storage: { - description: 'File storage configuration', + description: "File storage configuration", default: :file, format: :symbol, allowed: %i[file fog], writable: false }, attachments_storage_path: { - description: 'File storage disk location (only applicable for local file storage)', + description: "File storage disk location (only applicable for local file storage)", format: :string, default: nil, writable: false }, attachments_grace_period: { - description: 'Time in minutes to wait before uploaded files not attached to any container are removed', + description: "Time in minutes to wait before uploaded files not attached to any container are removed", default: 180 }, antivirus_scan_mode: { - description: 'Virus scanning option for files uploaded to OpenProject', + description: "Virus scanning option for files uploaded to OpenProject", format: :symbol, default: :disabled, allowed: %i[disabled clamav_socket clamav_host] }, antivirus_scan_target: { - description: 'The socket or hostname to connect to ClamAV', + description: "The socket or hostname to connect to ClamAV", format: :string, default: nil }, antivirus_scan_action: { - description: 'Virus scanning action for found infected files', + description: "Virus scanning action for found infected files", format: :symbol, default: :quarantine, allowed: %i[quarantine delete] }, auth_source_sso: { - description: 'Configuration for Header-based Single Sign-On', + description: "Configuration for Header-based Single Sign-On", format: :hash, default: nil, writable: false # config is cached globally so let's make it not writable @@ -123,7 +123,7 @@ class Definition # user: admin # password: 123456 authentication: { - description: 'Configuration options for global basic auth', + description: "Configuration options for global basic auth", format: :hash, default: nil }, @@ -138,12 +138,12 @@ class Definition allowed: [1, 7, 14, 30, 60, 90, 365] }, autologin_cookie_name: { - description: 'Cookie name for autologin cookie', - default: 'autologin' + description: "Cookie name for autologin cookie", + default: "autologin" }, autologin_cookie_path: { - description: 'Cookie path for autologin cookie', - default: '/' + description: "Cookie path for autologin cookie", + default: "/" }, available_languages: { format: :array, @@ -153,33 +153,33 @@ class Definition allowed: -> { Redmine::I18n.all_languages } }, avatar_link_expiry_seconds: { - description: 'Cache duration for avatar image API responses', + description: "Cache duration for avatar image API responses", default: 24.hours.to_i }, # Allow users with the required permissions to create backups via the web interface or API. backup_enabled: { - description: 'Enable application backups through the UI', + description: "Enable application backups through the UI", default: true }, backup_daily_limit: { - description: 'Maximum number of application backups allowed per day', + description: "Maximum number of application backups allowed per day", default: 3 }, backup_initial_waiting_period: { - description: 'Wait time before newly created backup tokens are usable', + description: "Wait time before newly created backup tokens are usable", default: 24.hours, format: :integer }, backup_include_attachments: { - description: 'Allow inclusion of attachments in application backups', + description: "Allow inclusion of attachments in application backups", default: true }, backup_attachment_size_max_sum_mb: { - description: 'Maximum limit of attachment size to include into application backups', + description: "Maximum limit of attachment size to include into application backups", default: 1024 }, blacklisted_routes: { - description: 'Blocked routes to prevent access to certain modules or pages', + description: "Blocked routes to prevent access to certain modules or pages", default: [], writable: false # used in initializer }, @@ -187,19 +187,19 @@ class Definition default: true }, boards_demo_data_available: { - description: 'Internal setting determining availability of demo seed data', + description: "Internal setting determining availability of demo seed data", default: false }, brute_force_block_minutes: { - description: 'Number of minutes to block users after presumed brute force attack', + description: "Number of minutes to block users after presumed brute force attack", default: 30 }, brute_force_block_after_failed_logins: { - description: 'Number of login attempts per user before assuming brute force attack', + description: "Number of login attempts per user before assuming brute force attack", default: 20 }, cache_expires_in_seconds: { - description: 'Expiration time for memcache entries, empty for no expiry be default', + description: "Expiration time for memcache entries, empty for no expiry be default", format: :integer, default: nil, writable: false @@ -209,40 +209,40 @@ class Definition }, # use dalli defaults for memcache cache_memcache_server: { - description: 'The memcache server host and IP', + description: "The memcache server host and IP", format: :string, default: nil, writable: false }, cache_redis_url: { - description: 'URL to the redis cache server', + description: "URL to the redis cache server", format: :string, default: nil, writable: false }, cache_namespace: { format: :string, - description: 'Namespace for cache keys, useful when multiple applications use a single memcache server', + description: "Namespace for cache keys, useful when multiple applications use a single memcache server", default: nil, writable: false }, commit_fix_done_ratio: { - description: 'Progress to apply when commit fixes work package', + description: "Progress to apply when commit fixes work package", default: 100 }, commit_fix_keywords: { - description: 'Keywords to look for in commit for fixing work packages', - default: 'fixes,closes' + description: "Keywords to look for in commit for fixing work packages", + default: "fixes,closes" }, commit_fix_status_id: { - description: 'Assigned status when fixing keyword is found', + description: "Assigned status when fixing keyword is found", format: :integer, default: nil, allowed: -> { Status.pluck(:id) + [nil] } }, commit_logs_encoding: { description: "Encoding used to convert commit logs to UTF-8", - default: 'UTF-8' + default: "UTF-8" }, commit_logtime_activity_id: { description: :setting_commit_logtime_activity_id, @@ -256,7 +256,7 @@ class Definition }, commit_ref_keywords: { description: "Keywords used in commits for referencing work packages", - default: 'refs,references,IssueID' + default: "refs,references,IssueID" }, consent_decline_mail: { format: :string, @@ -283,7 +283,7 @@ class Definition default: true }, database_cipher_key: { - description: 'Encryption key for repository credentials', + description: "Encryption key for repository credentials", format: :string, default: nil, writable: false @@ -292,28 +292,28 @@ class Definition format: :string, default: nil, allowed: [ - '%Y-%m-%d', - '%d/%m/%Y', - '%d.%m.%Y', - '%d-%m-%Y', - '%m/%d/%Y', - '%d %b %Y', - '%d %B %Y', - '%b %d, %Y', - '%B %d, %Y' + "%Y-%m-%d", + "%d/%m/%Y", + "%d.%m.%Y", + "%d-%m-%Y", + "%m/%d/%Y", + "%d %b %Y", + "%d %B %Y", + "%b %d, %Y", + "%B %d, %Y" ].freeze }, default_auto_hide_popups: { - description: 'Whether to automatically hide success notifications by default', + description: "Whether to automatically hide success notifications by default", default: true }, # user configuration default_comment_sort_order: { - description: 'Default sort order for activities', - default: 'asc' + description: "Default sort order for activities", + default: "asc" }, default_language: { - default: 'en', + default: "en", allowed: -> { Redmine::I18n.all_languages } }, default_projects_modules: { @@ -333,7 +333,7 @@ class Definition default: false }, development_highlight_enabled: { - description: 'Enable highlighting of development environment', + description: "Enable highlighting of development environment", default: -> { Rails.env.development? }, format: :boolean }, @@ -341,18 +341,18 @@ class Definition default: 1500 }, direct_uploads: { - description: 'Enable direct uploads to AWS S3. Only applicable with enabled Fog / AWS S3 configuration', + description: "Enable direct uploads to AWS S3. Only applicable with enabled Fog / AWS S3 configuration", default: true, writable: false }, disable_browser_cache: { - description: 'Prevent browser from caching any logged-in responses for security reasons', + description: "Prevent browser from caching any logged-in responses for security reasons", default: true, writable: false }, # allow to disable default modules disabled_modules: { - description: 'A list of module names to prevent access to in the application', + description: "A list of module names to prevent access to in the application", default: [], allowed: -> { OpenProject::AccessControl.available_project_modules.map(&:to_s) }, writable: false # setting stored in global variable @@ -362,48 +362,48 @@ class Definition default: false }, disable_password_login: { - description: 'Disable internal logins and instead only allow SSO through OmniAuth.', + description: "Disable internal logins and instead only allow SSO through OmniAuth.", default: false }, display_subprojects_work_packages: { default: true }, drop_old_sessions_on_logout: { - description: 'Destroy all sessions for current_user on logout', + description: "Destroy all sessions for current_user on logout", default: true }, drop_old_sessions_on_login: { - description: 'Destroy all sessions for current_user on login', + description: "Destroy all sessions for current_user on login", default: false }, edition: { format: :string, - default: 'standard', - description: 'OpenProject edition mode', + default: "standard", + description: "OpenProject edition mode", writable: false, allowed: %w[standard bim] }, ee_manager_visible: { - description: 'Show or hide the Enterprise configuration page and enterprise banners', + description: "Show or hide the Enterprise configuration page and enterprise banners", default: true, writable: false }, enable_internal_assets_server: { - description: 'Serve assets through the Rails internal asset server', + description: "Serve assets through the Rails internal asset server", default: false, writable: false }, # email configuration email_delivery_configuration: { - default: 'inapp', + default: "inapp", allowed: %w[inapp legacy], writable: false, - env_alias: 'EMAIL_DELIVERY_CONFIGURATION' + env_alias: "EMAIL_DELIVERY_CONFIGURATION" }, email_delivery_method: { format: :symbol, default: nil, - env_alias: 'EMAIL_DELIVERY_METHOD' + env_alias: "EMAIL_DELIVERY_METHOD" }, emails_salutation: { allowed: %w[firstname name], @@ -411,12 +411,12 @@ class Definition }, emails_footer: { default: { - 'en' => '' + "en" => "" } }, emails_header: { default: { - 'en' => '' + "en" => "" } }, # use email address as login, hide login in registration form @@ -432,18 +432,18 @@ class Definition }, # Allow connections for trial creation and booking enterprise_trial_creation_host: { - description: 'Host for EE trial service', - default: 'https://start.openproject.com', + description: "Host for EE trial service", + default: "https://start.openproject.com", writable: false }, enterprise_chargebee_site: { - description: 'Site name for EE trial service', - default: 'openproject-enterprise', + description: "Site name for EE trial service", + default: "openproject-enterprise", writable: false }, enterprise_plan: { - description: 'Default EE selected plan', - default: 'enterprise-on-premises---euro---1-year', + description: "Default EE selected plan", + default: "enterprise-on-premises---euro---1-year", writable: false }, feeds_enabled: { @@ -463,54 +463,54 @@ class Definition allowed: [1, 4] }, fog: { - description: 'Configure fog, e.g. when using an S3 uploader', + description: "Configure fog, e.g. when using an S3 uploader", default: {} }, fog_download_url_expires_in: { - description: 'Expiration time in seconds of created shared presigned URLs', + description: "Expiration time in seconds of created shared presigned URLs", default: 21600 # 6h by default as 6 hours is max in S3 when using IAM roles }, # Additional / overridden help links force_help_link: { - description: 'You can set a custom URL for the help button in application header menu.', + description: "You can set a custom URL for the help button in application header menu.", format: :string, default: nil }, force_formatting_help_link: { - description: 'You can set a custom URL for the help button in the WYSIWYG editor.', + description: "You can set a custom URL for the help button in the WYSIWYG editor.", format: :string, default: nil }, forced_single_page_size: { - description: 'Forced page size for manually sorted work package views', + description: "Forced page size for manually sorted work package views", default: 250 }, good_job_queues: { - description: '', + description: "", format: :string, writable: false, - default: '*' + default: "*" }, good_job_max_threads: { - description: '', + description: "", format: :integer, writable: false, default: 20 }, good_job_max_cache: { - description: '', + description: "", format: :integer, writable: false, default: 10_000 }, good_job_enable_cron: { - description: '', + description: "", format: :boolean, writable: false, default: true }, good_job_cleanup_preserved_jobs_before_seconds_ago: { - description: '', + description: "", format: :integer, writable: false, default: 7.days @@ -520,40 +520,40 @@ class Definition }, # Health check configuration health_checks_authentication_password: { - description: 'Add an authentication challenge for the /health_check endpoint', + description: "Add an authentication challenge for the /health_check endpoint", format: :string, default: nil }, ## Maximum number of minutes that jobs have not yet run after their designated 'run_at' time health_checks_jobs_never_ran_minutes_ago: { - description: 'Set threshold of outstanding background jobs to fail health check', + description: "Set threshold of outstanding background jobs to fail health check", format: :integer, default: 5 }, ## Maximum number of unprocessed requests in puma's backlog. health_checks_backlog_threshold: { - description: 'Set threshold of outstanding HTTP requests to fail health check', + description: "Set threshold of outstanding HTTP requests to fail health check", format: :integer, default: 20 }, # Default gravatar image, set to something other than 404 # to ensure a default is returned gravatar_fallback_image: { - description: 'Set default gravatar image fallback', - default: '404' + description: "Set default gravatar image fallback", + default: "404" }, hidden_menu_items: { - description: 'Hide menu items in the menu sidebar for each main menu (such as Administration and Projects).', + description: "Hide menu items in the menu sidebar for each main menu (such as Administration and Projects).", default: {}, writable: false # cached in global variable }, impressum_link: { - description: 'Impressum link to be set, hidden by default', + description: "Impressum link to be set, hidden by default", format: :string, default: nil }, installation_type: { - default: 'manual', + default: "manual", writable: false }, installation_uuid: { @@ -561,7 +561,7 @@ class Definition default: nil }, internal_password_confirmation: { - description: 'Require password confirmations for certain administrative actions', + description: "Require password confirmations for certain administrative actions", default: true }, invitation_expiration_days: { @@ -571,20 +571,20 @@ class Definition default: 5 }, ldap_force_no_page: { - description: 'Force LDAP to respond as a single page, in case paged responses do not work with your server.', + description: "Force LDAP to respond as a single page, in case paged responses do not work with your server.", format: :string, default: nil }, ldap_groups_disable_sync_job: { - description: 'Deactivate regular synchronization job for groups in case scheduled as a separate cronjob', + description: "Deactivate regular synchronization job for groups in case scheduled as a separate cronjob", default: false }, ldap_users_disable_sync_job: { - description: 'Deactivate user attributes synchronization from LDAP', + description: "Deactivate user attributes synchronization from LDAP", default: false }, ldap_users_sync_status: { - description: 'Enable user status (locked/unlocked) synchronization from LDAP', + description: "Enable user status (locked/unlocked) synchronization from LDAP", format: :boolean, default: false }, @@ -594,8 +594,8 @@ class Definition writable: true }, log_level: { - description: 'Set the OpenProject logger level', - default: Rails.env.development? ? 'debug' : 'info', + description: "Set the OpenProject logger level", + default: Rails.env.development? ? "debug" : "info", allowed: %w[debug info warn error fatal], writable: false }, @@ -603,14 +603,14 @@ class Definition default: false }, lograge_enabled: { - description: 'Use lograge formatter for outputting logs', + description: "Use lograge formatter for outputting logs", default: true, format: :boolean, writable: false }, lograge_formatter: { - description: 'Lograge formatter to use for outputting logs', - default: 'key_value', + description: "Lograge formatter to use for outputting logs", + default: "key_value", format: :string, writable: false }, @@ -618,42 +618,42 @@ class Definition default: true }, lookbook_enabled: { - description: 'Enable the Lookbook component documentation tool. Discouraged for production environments.', + description: "Enable the Lookbook component documentation tool. Discouraged for production environments.", default: -> { Rails.env.development? }, format: :boolean }, lost_password: { - description: 'Activate or deactivate lost password form', + description: "Activate or deactivate lost password form", default: true }, mail_from: { - default: 'openproject@example.net' + default: "openproject@example.net" }, mail_handler_api_key: { format: :string, default: nil }, mail_handler_body_delimiters: { - default: '' + default: "" }, mail_handler_body_delimiter_regex: { - default: '' + default: "" }, mail_handler_ignore_filenames: { - default: 'signature.asc' + default: "signature.asc" }, mail_suffix_separators: { - default: '+' + default: "+" }, main_content_language: { - default: 'english', - description: 'Main content language for PostgreSQL full text features', + default: "english", + description: "Main content language for PostgreSQL full text features", writable: false, allowed: %w[danish dutch english finnish french german hungarian italian norwegian portuguese romanian russian simple spanish swedish turkish] }, migration_check_on_exceptions: { - description: 'Check for missing migrations in internal errors', + description: "Check for missing migrations in internal errors", default: true, writable: false }, @@ -671,12 +671,12 @@ class Definition default: 60000 }, oauth_allow_remapping_of_existing_users: { - description: 'When set to false, prevent users from other identity providers to take over accounts connected ' \ - 'to another identity provider.', + description: "When set to false, prevent users from other identity providers to take over accounts connected " \ + "to another identity provider.", default: true }, omniauth_direct_login_provider: { - description: 'Clicking on login sends a login request to the specified OmniAuth provider.', + description: "Clicking on login sends a login request to the specified OmniAuth provider.", format: :string, default: nil }, @@ -687,11 +687,11 @@ class Definition writable: false # this changes a global variable and must therefore not be writable at runtime }, onboarding_video_url: { - description: 'Onboarding guide instructional video URL', - default: 'https://player.vimeo.com/video/163426858?autoplay=1' + description: "Onboarding guide instructional video URL", + default: "https://player.vimeo.com/video/163426858?autoplay=1" }, onboarding_enabled: { - description: 'Enable or disable onboarding guided tour for new users', + description: "Enable or disable onboarding guided tour for new users", default: true }, password_active_rules: { @@ -714,7 +714,7 @@ class Definition # Requires a migration to be written # replace Setting#per_page_options_array per_page_options: { - default: '20, 100' + default: "20, 100" }, plain_text_mail: { default: false @@ -724,63 +724,63 @@ class Definition format: :string }, rails_asset_host: { - description: 'Custom asset hostname for serving assets (e.g., Cloudfront)', + description: "Custom asset hostname for serving assets (e.g., Cloudfront)", format: :string, default: nil, writable: false }, rails_cache_store: { - description: 'Set cache store implemenation to use with OpenProject', + description: "Set cache store implemenation to use with OpenProject", format: :symbol, default: :file_store, writable: false, allowed: %i[file_store memcache redis] }, rails_relative_url_root: { - description: 'Set a URL prefix / base path to run OpenProject under, e.g., host.tld/openproject', - default: '', + description: "Set a URL prefix / base path to run OpenProject under, e.g., host.tld/openproject", + default: "", writable: false }, https: { - description: 'Set assumed connection security for the Rails processes', + description: "Set assumed connection security for the Rails processes", format: :boolean, default: -> { Rails.env.production? }, writable: false }, hsts: { - description: 'Allow disabling of HSTS headers and http -> https redirects', + description: "Allow disabling of HSTS headers and http -> https redirects", format: :boolean, default: true, writable: false }, home_url: { - description: 'Override default link when clicking on the top menu logo (Homescreen by default).', + description: "Override default link when clicking on the top menu logo (Homescreen by default).", format: :string, default: nil }, httpx_connect_timeout: { - description: '', + description: "", format: :float, writable: false, allowed: (0..), default: 3 }, httpx_read_timeout: { - description: '', + description: "", format: :float, writable: false, allowed: (0..), default: 3 }, httpx_write_timeout: { - description: '', + description: "", format: :float, writable: false, allowed: (0..), default: 3 }, httpx_keep_alive_timeout: { - description: '', + description: "", format: :float, writable: false, allowed: (0..), @@ -788,27 +788,27 @@ class Definition }, rate_limiting: { default: {}, - description: 'Configure rate limiting for various endpoint rules. See configuration documentation for details.' + description: "Configure rate limiting for various endpoint rules. See configuration documentation for details." }, registration_footer: { default: { - 'en' => '' + "en" => "" } }, remote_storage_upload_host: { format: :string, default: nil, writable: false, - description: 'Host the frontend uses to upload files to, which has to be added to the CSP.' + description: "Host the frontend uses to upload files to, which has to be added to the CSP." }, remote_storage_download_host: { format: :string, default: nil, writable: false, - description: 'Host the frontend uses to download files, which has to be added to the CSP.' + description: "Host the frontend uses to download files, which has to be added to the CSP." }, report_incoming_email_errors: { - description: 'Respond to incoming mails with error details', + description: "Respond to incoming mails with error details", default: true }, repositories_automatic_managed_vendor: { @@ -854,7 +854,7 @@ class Definition writable: false }, scm_local_checkout_path: { - default: 'repositories', # relative to OpenProject directory + default: "repositories", # relative to OpenProject directory writable: false }, scm_subversion_command: { @@ -867,32 +867,32 @@ class Definition default: true }, security_badge_url: { - description: 'URL of the update check badge', + description: "URL of the update check badge", default: "https://releases.openproject.com/v1/check.svg", writable: false }, seed_admin_user_password: { description: 'Password to set for the initially created admin user (Login remains "admin").', - default: 'admin', + default: "admin", writable: false }, seed_admin_user_mail: { - description: 'E-mail to set for the initially created admin user.', - default: 'admin@example.net', + description: "E-mail to set for the initially created admin user.", + default: "admin@example.net", writable: false }, seed_admin_user_name: { - description: 'Name to set for the initially created admin user.', - default: 'OpenProject Admin', + description: "Name to set for the initially created admin user.", + default: "OpenProject Admin", writable: false }, seed_admin_user_password_reset: { - description: 'Whether to force a password reset for the initially created admin user.', + description: "Whether to force a password reset for the initially created admin user.", default: true, writable: false }, seed_ldap: { - description: 'Provide an LDAP connection and sync settings through ENV', + description: "Provide an LDAP connection and sync settings through ENV", writable: false, default: nil, format: :hash @@ -901,12 +901,12 @@ class Definition default: 2 }, sendmail_arguments: { - description: 'Arguments to call sendmail with in case it is configured as outgoing email setup', + description: "Arguments to call sendmail with in case it is configured as outgoing email setup", format: :string, default: "-i" }, sendmail_location: { - description: 'Location of sendmail to call if it is configured as outgoing email setup', + description: "Location of sendmail to call if it is configured as outgoing email setup", format: :string, default: "/usr/sbin/sendmail" }, @@ -914,11 +914,11 @@ class Definition appsignal_frontend_key: { format: :string, default: nil, - description: 'Appsignal API key for JavaScript error reporting' + description: "Appsignal API key for JavaScript error reporting" }, session_cookie_name: { - description: 'Set session cookie name', - default: '_open_project_session' + description: "Set session cookie name", + default: "_open_project_session" }, session_ttl_enabled: { default: false @@ -927,44 +927,44 @@ class Definition default: 120 }, show_community_links: { - description: 'Enable or disable links to OpenProject community instances', + description: "Enable or disable links to OpenProject community instances", default: true }, show_product_version: { - description: 'Show product version information in the administration section', + description: "Show product version information in the administration section", default: true }, show_pending_migrations_warning: { - description: 'Enable or disable warning bar in case of pending migrations', + description: "Enable or disable warning bar in case of pending migrations", default: true, writable: false }, show_setting_mismatch_warning: { - description: 'Show mismatched protocol/hostname warning. In cases where they must differ this can be disabled', + description: "Show mismatched protocol/hostname warning. In cases where they must differ this can be disabled", default: true }, # Render storage information show_storage_information: { - description: 'Show available and taken storage information under administration / info', + description: "Show available and taken storage information under administration / info", default: true }, show_warning_bars: { - description: 'Render warning bars (pending migrations, deprecation, unsupported browsers)', + description: "Render warning bars (pending migrations, deprecation, unsupported browsers)", # Hide warning bars by default in tests as they might overlay other elements default: -> { !Rails.env.test? } }, smtp_authentication: { format: :string, - default: 'plain', - env_alias: 'SMTP_AUTHENTICATION' + default: "plain", + env_alias: "SMTP_AUTHENTICATION" }, smtp_enable_starttls_auto: { format: :boolean, default: false, - env_alias: 'SMTP_ENABLE_STARTTLS_AUTO' + env_alias: "SMTP_ENABLE_STARTTLS_AUTO" }, smtp_openssl_verify_mode: { - description: 'Globally set verify mode for OpenSSL. Careful: Setting to none will disable any SSL verification!', + description: "Globally set verify mode for OpenSSL. Careful: Setting to none will disable any SSL verification!", format: :string, default: "peer", allowed: %w[none peer client_once fail_if_no_peer_cert], @@ -973,43 +973,43 @@ class Definition smtp_ssl: { format: :boolean, default: false, - env_alias: 'SMTP_SSL' + env_alias: "SMTP_SSL" }, smtp_address: { format: :string, - default: '', - env_alias: 'SMTP_ADDRESS' + default: "", + env_alias: "SMTP_ADDRESS" }, smtp_domain: { format: :string, - default: 'your.domain.com', - env_alias: 'SMTP_DOMAIN' + default: "your.domain.com", + env_alias: "SMTP_DOMAIN" }, smtp_user_name: { format: :string, - default: '', - env_alias: 'SMTP_USER_NAME' + default: "", + env_alias: "SMTP_USER_NAME" }, smtp_port: { format: :integer, default: 587, - env_alias: 'SMTP_PORT' + env_alias: "SMTP_PORT" }, smtp_password: { format: :string, - default: '', - env_alias: 'SMTP_PASSWORD' + default: "", + env_alias: "SMTP_PASSWORD" }, software_name: { - description: 'Override software application name', - default: 'OpenProject' + description: "Override software application name", + default: "OpenProject" }, software_url: { - description: 'Override software application URL', - default: 'https://www.openproject.org/' + description: "Override software application URL", + default: "https://www.openproject.org/" }, sql_slow_query_threshold: { - description: 'Time limit in ms after which queries will be logged as slow queries', + description: "Time limit in ms after which queries will be logged as slow queries", default: 2000, writable: false }, @@ -1019,19 +1019,19 @@ class Definition allowed: [1, 6, 7] }, statsd: { - description: 'enable statsd metrics (currently puma only) by configuring host', + description: "enable statsd metrics (currently puma only) by configuring host", default: { - 'host' => nil, - 'port' => 8125 + "host" => nil, + "port" => 8125 }, writable: false }, sys_api_enabled: { - description: 'Enable internal system API for setting up managed repositories', + description: "Enable internal system API for setting up managed repositories", default: false }, sys_api_key: { - description: 'Internal system API key for setting up managed repositories', + description: "Internal system API key for setting up managed repositories", default: nil, format: :string }, @@ -1039,8 +1039,8 @@ class Definition format: :string, default: nil, allowed: [ - '%H:%M', - '%I:%M %p' + "%H:%M", + "%I:%M %p" ].freeze }, user_default_timezone: { @@ -1052,10 +1052,10 @@ class Definition default: false }, user_default_theme: { - default: 'light', + default: "light", format: :string, allowed: -> do - UserPreferences::Schema.schema.dig('definitions', 'UserPreferences', 'properties', 'theme', 'enum') + UserPreferences::Schema.schema.dig("definitions", "UserPreferences", "properties", "theme", "enum") end }, users_deletable_by_self: { @@ -1066,13 +1066,13 @@ class Definition allowed: -> { User::USER_FORMATS_STRUCTURE.keys } }, web: { - description: 'Web worker count and threads configuration', + description: "Web worker count and threads configuration", default: { - 'workers' => 2, - 'timeout' => 120, - 'wait_timeout' => 10, - 'min_threads' => 4, - 'max_threads' => 16 + "workers" => 2, + "timeout" => 120, + "wait_timeout" => 10, + "min_threads" => 4, + "max_threads" => 16 }, writable: false }, @@ -1088,7 +1088,7 @@ class Definition default: false }, work_package_done_ratio: { - default: 'field', + default: "field", allowed: %w[field status disabled] }, work_packages_projects_export_limit: { @@ -1105,7 +1105,7 @@ class Definition }, work_package_list_default_highlighting_mode: { format: :string, - default: -> { EnterpriseToken.allows_to?(:conditional_highlighting) ? 'inline' : 'none' }, + default: -> { EnterpriseToken.allows_to?(:conditional_highlighting) ? "inline" : "none" }, allowed: -> { Query::QUERY_HIGHLIGHTING_MODES.map(&:to_s) }, writable: -> { EnterpriseToken.allows_to?(:conditional_highlighting) } }, @@ -1117,14 +1117,14 @@ class Definition default: false }, working_days: { - description: 'Set working days of the week (Array of 1 to 7, where 1=Monday, 7=Sunday)', + description: "Set working days of the week (Array of 1 to 7, where 1=Monday, 7=Sunday)", format: :array, allowed: Array(1..7), default: Array(1..5) # Sat, Sun being non-working days, }, youtube_channel: { - description: 'Link to YouTube channel in help menu', - default: 'https://www.youtube.com/c/OpenProjectCommunity' + description: "Link to YouTube channel in help menu", + default: "https://www.youtube.com/c/OpenProjectCommunity" } }.freeze @@ -1289,7 +1289,7 @@ def all def file_config @file_config ||= begin - filename = Rails.root.join('config/configuration.yml') + filename = Rails.root.join("config/configuration.yml") file_config = {} @@ -1314,8 +1314,8 @@ def override_value(definition) end def override_value_from_file(definition) - envs = ['default', Rails.env] - envs.delete('default') if Rails.env.test? # The test setup should govern the configuration + envs = ["default", Rails.env] + envs.delete("default") if Rails.env.test? # The test setup should govern the configuration envs.each do |env| next unless (env_config = file_config[env]) next unless env_config.has_key?(definition.name) @@ -1379,7 +1379,7 @@ def path_to_hash(*path) end def unescape_underscores(path_segment) - path_segment.gsub '__', '_' + path_segment.gsub "__", "_" end def find_env_var_override(definition) @@ -1449,7 +1449,7 @@ def env_name_alias(definition) def extract_value_from_env(env_var_name, env_var_value) # YAML parses '' as false, but empty ENV variables will be passed as that. # To specify specific values, one can use !!str (-> '') or !!null (-> nil) - return env_var_value if env_var_value == '' + return env_var_value if env_var_value == "" parsed = load_yaml(env_var_value) diff --git a/config/initializers/menus.rb b/config/initializers/menus.rb index a5ce41ce3da2..ec7662bd4824 100644 --- a/config/initializers/menus.rb +++ b/config/initializers/menus.rb @@ -26,39 +26,39 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require 'redmine/menu_manager' +require "redmine/menu_manager" Redmine::MenuManager.map :top_menu do |menu| # projects menu will be added by # Redmine::MenuManager::TopMenuHelper#render_projects_top_menu_node menu.push :projects, - { controller: '/projects', project_id: nil, action: 'index' }, + { controller: "/projects", project_id: nil, action: "index" }, context: :modules, - caption: I18n.t('label_projects_menu'), - icon: 'projects', + caption: I18n.t("label_projects_menu"), + icon: "projects", if: Proc.new { User.current.logged? || !Setting.login_required? } menu.push :activity, - { controller: '/activities', action: 'index' }, + { controller: "/activities", action: "index" }, context: :modules, - icon: 'checkmark' + icon: "checkmark" menu.push :work_packages, - { controller: '/work_packages', project_id: nil, state: nil, action: 'index' }, + { controller: "/work_packages", project_id: nil, state: nil, action: "index" }, context: :modules, - caption: I18n.t('label_work_package_plural'), - icon: 'work-packages', + caption: I18n.t("label_work_package_plural"), + icon: "work-packages", if: Proc.new { (User.current.logged? || !Setting.login_required?) && User.current.allowed_in_any_work_package?(:view_work_packages) } menu.push :news, - { controller: '/news', project_id: nil, action: 'index' }, + { controller: "/news", project_id: nil, action: "index" }, context: :modules, - caption: I18n.t('label_news_plural'), - icon: 'news', + caption: I18n.t("label_news_plural"), + icon: "news", if: Proc.new { (User.current.logged? || !Setting.login_required?) && User.current.allowed_in_any_project?(:view_news) @@ -67,17 +67,17 @@ menu.push :help, OpenProject::Static::Links.help_link, last: true, - caption: '', - icon: 'help op-app-help--icon', + caption: "", + icon: "help op-app-help--icon", html: { accesskey: OpenProject::AccessKeys.key_for(:help), - title: I18n.t('label_help'), - target: '_blank' } + title: I18n.t("label_help"), + target: "_blank" } end Redmine::MenuManager.map :quick_add_menu do |menu| menu.push :new_project, Proc.new { |project| - { controller: '/projects', action: :new, project_id: nil, parent_id: project&.id } + { controller: "/projects", action: :new, project_id: nil, parent_id: project&.id } }, caption: ->(*) { Project.model_name.human }, icon: "add", @@ -93,32 +93,32 @@ menu.push :invite_user, nil, caption: :label_invite_user, - icon: 'user-plus', + icon: "user-plus", html: { - 'invite-user-modal-augment': 'invite-user-modal-augment' + "invite-user-modal-augment": "invite-user-modal-augment" }, if: Proc.new { User.current.allowed_in_any_project?(:manage_members) } end Redmine::MenuManager.map :account_menu do |menu| menu.push :timers, - { controller: '/my/timer', action: 'show' }, - partial: '/my/timer/menu' + { controller: "/my/timer", action: "show" }, + partial: "/my/timer/menu" menu.push :my_page, :my_page_path, - caption: I18n.t('js.my_page.label'), + caption: I18n.t("js.my_page.label"), if: Proc.new { User.current.logged? } menu.push :my_profile, - { controller: '/users', action: 'show', id: 'me' }, + { controller: "/users", action: "show", id: "me" }, caption: :label_my_activity, if: Proc.new { User.current.logged? } menu.push :my_account, - { controller: '/my', action: 'account' }, + { controller: "/my", action: "account" }, if: Proc.new { User.current.logged? } menu.push :administration, - { controller: '/admin', action: 'index' }, + { controller: "/admin", action: "index" }, if: Proc.new { - User.current.allowed_globally?({ controller: '/admin', action: 'index' }) + User.current.allowed_globally?({ controller: "/admin", action: "index" }) } menu.push :logout, :signout_path, @@ -128,53 +128,53 @@ Redmine::MenuManager.map :global_menu do |menu| # Homescreen menu.push :home, - { controller: '/homescreen', action: 'index' }, - icon: 'home', + { controller: "/homescreen", action: "index" }, + icon: "home", first: true # Projects menu.push :projects, - { controller: '/projects', project_id: nil, action: 'index' }, - caption: I18n.t('label_projects_menu'), - icon: 'projects', + { controller: "/projects", project_id: nil, action: "index" }, + caption: I18n.t("label_projects_menu"), + icon: "projects", after: :home, if: Proc.new { User.current.logged? || !Setting.login_required? } menu.push :projects_query_select, - { controller: '/projects', project_id: nil, action: 'index' }, + { controller: "/projects", project_id: nil, action: "index" }, parent: :projects, - partial: 'projects/menus/menu' + partial: "projects/menus/menu" # Activity menu.push :activity, - { controller: '/activities', action: 'index' }, - icon: 'checkmark', + { controller: "/activities", action: "index" }, + icon: "checkmark", after: :projects menu.push :activity_filters, - { controller: '/activities', action: 'index' }, + { controller: "/activities", action: "index" }, parent: :activity, - partial: 'activities/filters_menu' + partial: "activities/filters_menu" # Work packages menu.push :work_packages, - { controller: '/work_packages', action: 'index' }, + { controller: "/work_packages", action: "index" }, caption: :label_work_package_plural, - icon: 'view-list', + icon: "view-list", after: :activity menu.push :work_packages_query_select, - { controller: '/work_packages', action: 'index' }, + { controller: "/work_packages", action: "index" }, parent: :work_packages, - partial: 'work_packages/menu_query_select' + partial: "work_packages/menu_query_select" # News menu.push :news, - { controller: '/news', project_id: nil, action: 'index' }, - caption: I18n.t('label_news_plural'), - icon: 'news', + { controller: "/news", project_id: nil, action: "index" }, + caption: I18n.t("label_news_plural"), + icon: "news", after: :boards, if: Proc.new { (User.current.logged? || !Setting.login_required?) && @@ -184,194 +184,194 @@ Redmine::MenuManager.map :notifications_menu do |menu| menu.push :notification_grouping_select, - { controller: '/my', action: 'notifications' }, - partial: 'notifications/menu_notification_center' + { controller: "/my", action: "notifications" }, + partial: "notifications/menu_notification_center" end Redmine::MenuManager.map :my_menu do |menu| menu.push :account, - { controller: '/my', action: 'account' }, + { controller: "/my", action: "account" }, caption: :label_profile, - icon: 'user' + icon: "user" menu.push :settings, - { controller: '/my', action: 'settings' }, + { controller: "/my", action: "settings" }, caption: :label_setting_plural, - icon: 'settings2' + icon: "settings2" menu.push :password, - { controller: '/my', action: 'password' }, + { controller: "/my", action: "password" }, caption: :button_change_password, if: Proc.new { User.current.change_password_allowed? }, - icon: 'locked' + icon: "locked" menu.push :access_token, - { controller: '/my', action: 'access_token' }, - caption: I18n.t('my_account.access_tokens.access_tokens'), - icon: 'key' + { controller: "/my", action: "access_token" }, + caption: I18n.t("my_account.access_tokens.access_tokens"), + icon: "key" menu.push :sessions, - { controller: '/my/sessions', action: :index }, - caption: :'users.sessions.title', - icon: 'installation-services' + { controller: "/my/sessions", action: :index }, + caption: :"users.sessions.title", + icon: "installation-services" menu.push :notifications, - { controller: '/my', action: 'notifications' }, - caption: I18n.t('js.notifications.settings.title'), - icon: 'bell' + { controller: "/my", action: "notifications" }, + caption: I18n.t("js.notifications.settings.title"), + icon: "bell" menu.push :reminders, - { controller: '/my', action: 'reminders' }, - caption: I18n.t('js.reminders.settings.title'), - icon: 'email-alert' + { controller: "/my", action: "reminders" }, + caption: I18n.t("js.reminders.settings.title"), + icon: "email-alert" menu.push :delete_account, :delete_my_account_info_path, - caption: I18n.t('account.delete'), + caption: I18n.t("account.delete"), param: :user_id, if: Proc.new { Setting.users_deletable_by_self? }, last: :delete_account, - icon: 'delete' + icon: "delete" end Redmine::MenuManager.map :admin_menu do |menu| menu.push :admin_overview, - { controller: '/admin', action: :index }, + { controller: "/admin", action: :index }, if: Proc.new { User.current.admin? }, caption: :label_overview, - icon: 'home', + icon: "home", first: true menu.push :users, - { controller: '/users' }, + { controller: "/users" }, if: Proc.new { !User.current.admin? && (User.current.allowed_globally?(:manage_user) || User.current.allowed_globally?(:create_user)) }, caption: :label_user_plural, - icon: 'group' + icon: "group" menu.push :placeholder_users, - { controller: '/placeholder_users' }, + { controller: "/placeholder_users" }, if: Proc.new { !User.current.admin? && User.current.allowed_globally?(:manage_placeholder_user) }, caption: :label_placeholder_user_plural, - icon: 'group' + icon: "group" menu.push :users_and_permissions, - { controller: '/users' }, + { controller: "/users" }, if: Proc.new { User.current.admin? }, caption: :label_user_and_permission, - icon: 'group' + icon: "group" menu.push :user_settings, - { controller: '/admin/settings/users_settings', action: :show }, + { controller: "/admin/settings/users_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_users_settings, parent: :users_and_permissions menu.push :users, - { controller: '/users' }, + { controller: "/users" }, if: Proc.new { User.current.admin? }, caption: :label_user_plural, parent: :users_and_permissions menu.push :placeholder_users, - { controller: '/placeholder_users' }, + { controller: "/placeholder_users" }, if: Proc.new { User.current.admin? }, caption: :label_placeholder_user_plural, parent: :users_and_permissions, - enterprise_feature: 'placeholder_users' + enterprise_feature: "placeholder_users" menu.push :groups, - { controller: '/groups' }, + { controller: "/groups" }, if: Proc.new { User.current.admin? }, caption: :label_group_plural, parent: :users_and_permissions menu.push :roles, - { controller: '/roles' }, + { controller: "/roles" }, if: Proc.new { User.current.admin? }, caption: :label_role_and_permissions, parent: :users_and_permissions menu.push :permissions_report, - { controller: '/roles', action: 'report' }, + { controller: "/roles", action: "report" }, if: Proc.new { User.current.admin? }, caption: :label_permissions_report, parent: :users_and_permissions menu.push :user_avatars, - { controller: '/admin/settings', action: 'show_plugin', id: :openproject_avatars }, + { controller: "/admin/settings", action: "show_plugin", id: :openproject_avatars }, if: Proc.new { User.current.admin? }, caption: :label_avatar_plural, parent: :users_and_permissions menu.push :admin_work_packages, - { controller: '/admin/settings/work_packages_settings', action: :show }, + { controller: "/admin/settings/work_packages_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_work_package_plural, - icon: 'view-list' + icon: "view-list" menu.push :work_packages_setting, - { controller: '/admin/settings/work_packages_settings', action: :show }, + { controller: "/admin/settings/work_packages_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_setting_plural, parent: :admin_work_packages menu.push :types, - { controller: '/types' }, + { controller: "/types" }, if: Proc.new { User.current.admin? }, caption: :label_type_plural, parent: :admin_work_packages menu.push :statuses, - { controller: '/statuses' }, + { controller: "/statuses" }, if: Proc.new { User.current.admin? }, caption: :label_status, parent: :admin_work_packages, - html: { class: 'statuses' } + html: { class: "statuses" } menu.push :workflows, - { controller: '/workflows', action: 'edit' }, + { controller: "/workflows", action: "edit" }, if: Proc.new { User.current.admin? }, caption: Proc.new { Workflow.model_name.human }, parent: :admin_work_packages menu.push :custom_fields, - { controller: '/custom_fields' }, + { controller: "/custom_fields" }, if: Proc.new { User.current.admin? }, caption: :label_custom_field_plural, - icon: 'custom-fields', - html: { class: 'custom_fields' } + icon: "custom-fields", + html: { class: "custom_fields" } menu.push :custom_actions, - { controller: '/custom_actions' }, + { controller: "/custom_actions" }, if: Proc.new { User.current.admin? }, - caption: :'custom_actions.plural', + caption: :"custom_actions.plural", parent: :admin_work_packages, - enterprise_feature: 'custom_actions' + enterprise_feature: "custom_actions" menu.push :attribute_help_texts, - { controller: '/attribute_help_texts' }, - caption: :'attribute_help_texts.label_plural', - icon: 'help2', + { controller: "/attribute_help_texts" }, + caption: :"attribute_help_texts.label_plural", + icon: "help2", if: Proc.new { User.current.allowed_globally?(:edit_attribute_help_texts) } menu.push :attachments, - { controller: '/admin/settings/attachments_settings', action: :show }, - caption: :'attributes.attachments', - icon: 'attachment', + { controller: "/admin/settings/attachments_settings", action: :show }, + caption: :"attributes.attachments", + icon: "attachment", if: Proc.new { User.current.admin? } menu.push :attachments_settings, - { controller: '/admin/settings/attachments_settings', action: :show }, + { controller: "/admin/settings/attachments_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_setting_plural, parent: :attachments menu.push :virus_scanning_settings, - { controller: '/admin/settings/virus_scanning_settings', action: :show }, - caption: :'settings.antivirus.title', + { controller: "/admin/settings/virus_scanning_settings", action: :show }, + caption: :"settings.antivirus.title", parent: :attachments, - enterprise_feature: 'virus_scanning', + enterprise_feature: "virus_scanning", if: Proc.new { User.current.admin? } menu.push :attachment_quarantine, - { controller: '/admin/attachments/quarantined_attachments', action: :index }, - caption: :'antivirus_scan.quarantined_attachments.title', + { controller: "/admin/attachments/quarantined_attachments", action: :index }, + caption: :"antivirus_scan.quarantined_attachments.title", parent: :attachments, if: Proc.new { User.current.admin? && @@ -379,176 +379,176 @@ } menu.push :enumerations, - { controller: '/enumerations' }, + { controller: "/enumerations" }, if: Proc.new { User.current.admin? }, - icon: 'enumerations' + icon: "enumerations" menu.push :calendars_and_dates, - { controller: '/admin/settings/working_days_settings', action: :show }, + { controller: "/admin/settings/working_days_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_calendars_and_dates, - icon: 'calendar' + icon: "calendar" menu.push :working_days, - { controller: '/admin/settings/working_days_settings', action: :show }, + { controller: "/admin/settings/working_days_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_working_days, parent: :calendars_and_dates menu.push :date_format, - { controller: '/admin/settings/date_format_settings', action: :show }, + { controller: "/admin/settings/date_format_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_date_format, parent: :calendars_and_dates menu.push :icalendar, - { controller: '/admin/settings/icalendar_settings', action: :show }, + { controller: "/admin/settings/icalendar_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_calendar_subscriptions, parent: :calendars_and_dates menu.push :settings, - { controller: '/admin/settings/general_settings', action: :show }, + { controller: "/admin/settings/general_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_system_settings, - icon: 'settings2' + icon: "settings2" SettingsHelper.system_settings_tabs.each do |node| menu.push :"settings_#{node[:name]}", { controller: node[:controller], action: :show }, caption: node[:label], - if: Proc.new { User.current.admin? && node[:name] != 'experimental' }, + if: Proc.new { User.current.admin? && node[:name] != "experimental" }, parent: :settings end menu.push :mail_and_notifications, - { controller: '/admin/settings/aggregation_settings', action: :show }, + { controller: "/admin/settings/aggregation_settings", action: :show }, if: Proc.new { User.current.admin? }, - caption: :'menus.admin.mails_and_notifications', - icon: 'mail1' + caption: :"menus.admin.mails_and_notifications", + icon: "mail1" menu.push :notification_settings, - { controller: '/admin/settings/aggregation_settings', action: :show }, + { controller: "/admin/settings/aggregation_settings", action: :show }, if: Proc.new { User.current.admin? }, - caption: :'menus.admin.aggregation', + caption: :"menus.admin.aggregation", parent: :mail_and_notifications menu.push :mail_notifications, - { controller: '/admin/settings/mail_notifications_settings', action: :show }, + { controller: "/admin/settings/mail_notifications_settings", action: :show }, if: Proc.new { User.current.admin? }, - caption: :'menus.admin.mail_notification', + caption: :"menus.admin.mail_notification", parent: :mail_and_notifications menu.push :incoming_mails, - { controller: '/admin/settings/incoming_mails_settings', action: :show }, + { controller: "/admin/settings/incoming_mails_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_incoming_emails, parent: :mail_and_notifications menu.push :api_and_webhooks, - { controller: '/admin/settings/api_settings', action: :show }, + { controller: "/admin/settings/api_settings", action: :show }, if: Proc.new { User.current.admin? }, - caption: :'menus.admin.api_and_webhooks', - icon: 'relations' + caption: :"menus.admin.api_and_webhooks", + icon: "relations" menu.push :api, - { controller: '/admin/settings/api_settings', action: :show }, + { controller: "/admin/settings/api_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_api_access_key_type, parent: :api_and_webhooks menu.push :authentication, - { controller: '/admin/settings/authentication_settings', action: :show }, + { controller: "/admin/settings/authentication_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_authentication, - icon: 'two-factor-authentication' + icon: "two-factor-authentication" menu.push :authentication_settings, - { controller: '/admin/settings/authentication_settings', action: :show }, + { controller: "/admin/settings/authentication_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_setting_plural, parent: :authentication menu.push :ldap_authentication, - { controller: '/ldap_auth_sources', action: 'index' }, + { controller: "/ldap_auth_sources", action: "index" }, if: Proc.new { User.current.admin? && !OpenProject::Configuration.disable_password_login? }, parent: :authentication, caption: :label_ldap_auth_source_plural, - html: { class: 'server_authentication' }, + html: { class: "server_authentication" }, last: true menu.push :oauth_applications, - { controller: '/oauth/applications', action: 'index' }, + { controller: "/oauth/applications", action: "index" }, if: Proc.new { User.current.admin? }, parent: :authentication, - caption: :'oauth.application.plural', - html: { class: 'oauth_applications' } + caption: :"oauth.application.plural", + html: { class: "oauth_applications" } menu.push :announcements, - { controller: '/announcements', action: 'edit' }, + { controller: "/announcements", action: "edit" }, if: Proc.new { User.current.admin? }, caption: :label_announcement, - icon: 'news' + icon: "news" menu.push :plugins, - { controller: '/admin', action: 'plugins' }, + { controller: "/admin", action: "plugins" }, if: Proc.new { User.current.admin? }, last: true, - icon: 'plugins' + icon: "plugins" menu.push :backups, - { controller: '/admin/backups', action: 'show' }, + { controller: "/admin/backups", action: "show" }, if: Proc.new { OpenProject::Configuration.backup_enabled? && User.current.allowed_globally?(Backup.permission) }, caption: :label_backup, last: true, - icon: 'save' + icon: "save" menu.push :info, - { controller: '/admin', action: 'info' }, + { controller: "/admin", action: "info" }, if: Proc.new { User.current.admin? }, caption: :label_information_plural, last: true, - icon: 'info1' + icon: "info1" menu.push :custom_style, - { controller: '/custom_styles', action: :show }, + { controller: "/custom_styles", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_custom_style, - icon: 'design', - enterprise_feature: 'define_custom_style' + icon: "design", + enterprise_feature: "define_custom_style" menu.push :colors, - { controller: '/colors', action: 'index' }, + { controller: "/colors", action: "index" }, if: Proc.new { User.current.admin? }, caption: :label_color_plural, - icon: 'status' + icon: "status" menu.push :enterprise, - { controller: '/enterprises', action: :show }, + { controller: "/enterprises", action: :show }, caption: :label_enterprise_edition, - icon: 'enterprise-addons', + icon: "enterprise-addons", if: proc { User.current.admin? && OpenProject::Configuration.ee_manager_visible? } menu.push :admin_costs, - { controller: '/admin/settings', action: 'show_plugin', id: :costs }, + { controller: "/admin/settings", action: "show_plugin", id: :costs }, if: Proc.new { User.current.admin? }, caption: :project_module_costs, - icon: 'budget' + icon: "budget" menu.push :costs_setting, - { controller: '/admin/settings', action: 'show_plugin', id: :costs }, + { controller: "/admin/settings", action: "show_plugin", id: :costs }, if: Proc.new { User.current.admin? }, caption: :label_setting_plural, parent: :admin_costs menu.push :admin_backlogs, - { controller: '/backlogs_settings', action: :show }, + { controller: "/backlogs_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_backlogs, - icon: 'backlogs' + icon: "backlogs" menu.push :backlogs_settings, - { controller: '/backlogs_settings', action: :show }, + { controller: "/backlogs_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_setting_plural, parent: :admin_backlogs @@ -556,72 +556,72 @@ Redmine::MenuManager.map :project_menu do |menu| menu.push :activity, - { controller: '/activities', action: 'index' }, - if: Proc.new { |p| p.module_enabled?('activity') }, - icon: 'checkmark' + { controller: "/activities", action: "index" }, + if: Proc.new { |p| p.module_enabled?("activity") }, + icon: "checkmark" menu.push :activity_filters, - { controller: '/activities', action: 'index' }, - if: Proc.new { |p| p.module_enabled?('activity') }, + { controller: "/activities", action: "index" }, + if: Proc.new { |p| p.module_enabled?("activity") }, parent: :activity, - partial: 'activities/filters_menu' + partial: "activities/filters_menu" menu.push :roadmap, - { controller: '/versions', action: 'index' }, + { controller: "/versions", action: "index" }, if: Proc.new { |p| p.shared_versions.any? }, - icon: 'roadmap' + icon: "roadmap" menu.push :work_packages, - { controller: '/work_packages', action: 'index' }, + { controller: "/work_packages", action: "index" }, caption: :label_work_package_plural, - if: Proc.new { |p| p.module_enabled?('work_package_tracking') }, - icon: 'view-list', + if: Proc.new { |p| p.module_enabled?("work_package_tracking") }, + icon: "view-list", html: { - id: 'main-menu-work-packages', - 'wp-query-menu': 'wp-query-menu' + id: "main-menu-work-packages", + "wp-query-menu": "wp-query-menu" } menu.push :work_packages_query_select, - { controller: '/work_packages', action: 'index' }, + { controller: "/work_packages", action: "index" }, parent: :work_packages, - partial: 'work_packages/menu_query_select', + partial: "work_packages/menu_query_select", last: true, caption: :label_all_open_wps menu.push :news, - { controller: '/news', action: 'index' }, + { controller: "/news", action: "index" }, caption: :label_news_plural, - icon: 'news' + icon: "news" menu.push :forums, - { controller: '/forums', action: 'index', id: nil }, + { controller: "/forums", action: "index", id: nil }, caption: :label_forum_plural, - icon: 'ticket-note' + icon: "ticket-note" menu.push :repository, - { controller: '/repositories', action: :show }, + { controller: "/repositories", action: :show }, if: Proc.new { |p| p.repository && !p.repository.new_record? }, - icon: 'folder-open' + icon: "folder-open" # Wiki menu items are added by WikiMenuItemHelper menu.push :members, - { controller: '/members', action: 'index' }, + { controller: "/members", action: "index" }, caption: :label_member_plural, before: :settings, - icon: 'group' + icon: "group" menu.push :members_menu, - { controller: '/members', action: 'index' }, + { controller: "/members", action: "index" }, parent: :members, - partial: 'members/menus/menu', + partial: "members/menus/menu", caption: :label_member_plural menu.push :settings, - { controller: '/projects/settings/general', action: :show }, + { controller: "/projects/settings/general", action: :show }, caption: :label_project_settings, last: true, - icon: 'settings2', + icon: "settings2", allow_deeplink: true { @@ -636,7 +636,7 @@ storage: :label_required_disk_storage }.each do |key, caption| menu.push :"settings_#{key}", - { controller: "/projects/settings/#{key}", action: 'show' }, + { controller: "/projects/settings/#{key}", action: "show" }, caption:, parent: :settings end diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb index d76e3c19f52e..4098e318acf5 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -37,7 +37,7 @@ map.permission :archive_project, { - 'projects/archive': %i[create] + "projects/archive": %i[create] }, permissible_on: :project, require: :member @@ -45,7 +45,7 @@ map.permission :create_backup, { admin: %i[index], - 'admin/backups': %i[delete_token perform_token_reset reset_token show] + "admin/backups": %i[delete_token perform_token_reset reset_token show] }, permissible_on: :global, require: :loggedin, @@ -54,7 +54,7 @@ map.permission :create_user, { users: %i[index show new create resend_invitation], - 'users/memberships': %i[create], + "users/memberships": %i[create], admin: %i[index] }, permissible_on: :global, @@ -64,7 +64,7 @@ map.permission :manage_user, { users: %i[index show edit update change_status change_status_info], - 'users/memberships': %i[create update destroy], + "users/memberships": %i[create update destroy], admin: %i[index] }, permissible_on: :global, @@ -74,7 +74,7 @@ map.permission :manage_placeholder_user, { placeholder_users: %i[index show new create edit update deletion_info destroy], - 'placeholder_users/memberships': %i[create update destroy], + "placeholder_users/memberships": %i[create update destroy], admin: %i[index] }, permissible_on: :global, @@ -93,10 +93,10 @@ map.permission :edit_project, { - 'projects/settings/general': %i[show], - 'projects/settings/storage': %i[show], - 'projects/templated': %i[create destroy], - 'projects/identifier': %i[show update] + "projects/settings/general": %i[show], + "projects/settings/storage": %i[show], + "projects/templated": %i[create destroy], + "projects/identifier": %i[show update] }, permissible_on: :project, require: :member, @@ -104,7 +104,7 @@ map.permission :select_project_modules, { - 'projects/settings/modules': %i[show update] + "projects/settings/modules": %i[show update] }, permissible_on: :project, require: :member @@ -112,7 +112,7 @@ map.permission :manage_members, { members: %i[index new create update destroy autocomplete_for_member menu], - 'members/menus': %i[show] + "members/menus": %i[show] }, permissible_on: :project, require: :member, @@ -122,14 +122,14 @@ map.permission :view_members, { members: %i[index menu], - 'members/menus': %i[show] + "members/menus": %i[show] }, permissible_on: :project, contract_actions: { members: %i[read] } map.permission :manage_versions, { - 'projects/settings/versions': [:show], + "projects/settings/versions": [:show], versions: %i[new create edit update close_completed destroy] }, permissible_on: :project, @@ -137,14 +137,14 @@ map.permission :manage_types, { - 'projects/settings/types': %i[show update] + "projects/settings/types": %i[show update] }, permissible_on: :project, require: :member map.permission :select_custom_fields, { - 'projects/settings/custom_fields': %i[show update] + "projects/settings/custom_fields": %i[show update] }, permissible_on: :project, require: :member @@ -179,7 +179,7 @@ journals: %i[index], work_packages: %i[show index], work_packages_api: [:get], - 'work_packages/reports': %i[report report_details] + "work_packages/reports": %i[report report_details] }, permissible_on: %i[work_package project], contract_actions: { work_packages: %i[read] } @@ -192,7 +192,7 @@ wpt.permission :edit_work_packages, { - 'work_packages/bulk': %i[edit update] + "work_packages/bulk": %i[edit update] }, permissible_on: %i[work_package project], require: :member, @@ -200,7 +200,7 @@ contract_actions: { work_packages: %i[update] } wpt.permission :move_work_packages, - { 'work_packages/moves': %i[new create] }, + { "work_packages/moves": %i[new create] }, permissible_on: :project, require: :loggedin, dependencies: :view_work_packages, @@ -242,7 +242,7 @@ # WorkPackage categories wpt.permission :manage_categories, { - 'projects/settings/categories': [:show], + "projects/settings/categories": [:show], categories: %i[new create edit update destroy] }, permissible_on: :project, @@ -258,7 +258,7 @@ wpt.permission :delete_work_packages, { work_packages: :destroy, - 'work_packages/bulk': :destroy + "work_packages/bulk": :destroy }, permissible_on: :project, require: :member, @@ -304,8 +304,8 @@ map.permission :share_work_packages, { - 'work_packages/shares': %i[index create destroy update resend_invite], - 'work_packages/shares/bulk': %i[update destroy] + "work_packages/shares": %i[index create destroy update resend_invite], + "work_packages/shares/bulk": %i[update destroy] }, permissible_on: :project, dependencies: %i[edit_work_packages view_shared_work_packages], @@ -313,7 +313,7 @@ map.permission :view_shared_work_packages, { - 'work_packages/shares': %i[index] + "work_packages/shares": %i[index] }, permissible_on: :project, require: :member, @@ -350,13 +350,13 @@ news.permission :manage_news, { news: %i[new create edit update destroy preview], - 'news/comments': [:destroy] + "news/comments": [:destroy] }, permissible_on: :project, require: :member news.permission :comment_news, - { 'news/comments': :create }, + { "news/comments": :create }, permissible_on: :project end @@ -429,7 +429,7 @@ repo.permission :manage_repository, { repositories: %i[edit create update committers destroy_info destroy], - 'projects/settings/repository': :show + "projects/settings/repository": :show }, permissible_on: :project, require: :member diff --git a/config/locales/en.yml b/config/locales/en.yml index 91ab4632f334..7703ace3f93d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -35,7 +35,8 @@ en: admin: plugins: - no_results_title_text: There are currently no plugins available. + no_results_title_text: There are currently no plugins installed. + no_results_content_text: See our integrations and plugins page for more information. custom_styles: color_theme: "Color theme" color_theme_custom: "(Custom)" diff --git a/config/routes.rb b/config/routes.rb index bbf68291a86a..89c4963a26d3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,74 +27,74 @@ #++ Rails.application.routes.draw do - root to: 'homescreen#index', as: 'home' - rails_relative_url_root = OpenProject::Configuration['rails_relative_url_root'] || '' + root to: "homescreen#index", as: "home" + rails_relative_url_root = OpenProject::Configuration["rails_relative_url_root"] || "" # Route for error pages - get '/404', to: "errors#not_found" - get '/422', to: "errors#unacceptable" - get '/500', to: "errors#internal_error" + get "/404", to: "errors#not_found" + get "/422", to: "errors#unacceptable" + get "/500", to: "errors#internal_error" # Route for health_checks - get '/health_check' => 'ok_computer/ok_computer#show', check: 'web' + get "/health_check" => "ok_computer/ok_computer#show", check: "web" # Override the default `all` checks route to return the full check - get '/health_checks/all' => 'ok_computer/ok_computer#show', check: 'full' + get "/health_checks/all" => "ok_computer/ok_computer#show", check: "full" mount OkComputer::Engine, at: "/health_checks" - get "/api/docs" => 'api_docs#index' + get "/api/docs" => "api_docs#index" # Redirect deprecated issue links to new work packages uris - get '/issues(/)' => redirect("#{rails_relative_url_root}/work_packages") + get "/issues(/)" => redirect("#{rails_relative_url_root}/work_packages") # The URI.escape doesn't escape / unless you ask it to. # see https://github.com/rails/rails/issues/5688 - get '/issues/*rest' => redirect { |params, _req| + get "/issues/*rest" => redirect { |params, _req| "#{rails_relative_url_root}/work_packages/#{URI::RFC2396_Parser.new.escape(params[:rest])}" } # Respond with 410 gone for APIV2 calls - match '/api/v2(/*unmatched_route)', to: proc { [410, {}, ['']] }, via: :all - match '/assets/compiler.js.map', to: proc { [404, {}, ['']] }, via: :all + match "/api/v2(/*unmatched_route)", to: proc { [410, {}, [""]] }, via: :all + match "/assets/compiler.js.map", to: proc { [404, {}, [""]] }, via: :all # Redirect wp short url for work packages to full URL - get '/wp(/)' => redirect("#{rails_relative_url_root}/work_packages") - get '/wp/*rest' => redirect { |params, _req| + get "/wp(/)" => redirect("#{rails_relative_url_root}/work_packages") + get "/wp/*rest" => redirect { |params, _req| "#{rails_relative_url_root}/work_packages/#{URI::RFC2396_Parser.new.escape(params[:rest])}" } # Add catch method for Rack OmniAuth to allow route helpers # Note: This renders a 404 in rails but is caught by omniauth in Rack before - get '/auth/failure', to: 'account#omniauth_failure' - get '/auth/:provider', to: proc { [404, {}, ['']] }, as: 'omniauth_start' - match '/auth/:provider/callback', to: 'account#omniauth_login', as: 'omniauth_login', via: %i[get post] + get "/auth/failure", to: "account#omniauth_failure" + get "/auth/:provider", to: proc { [404, {}, [""]] }, as: "omniauth_start" + match "/auth/:provider/callback", to: "account#omniauth_login", as: "omniauth_login", via: %i[get post] # In case assets are actually delivered by a node server (e.g. in test env) # forward requests to the proxy if FrontendAssetHelper.assets_proxied? - match '/assets/frontend/*appendix', + match "/assets/frontend/*appendix", to: redirect("#{FrontendAssetHelper.cli_proxy}/assets/frontend/%{appendix}", status: 307), format: false, via: :all end - scope controller: 'account' do - get '/account/force_password_change', action: 'force_password_change' - post '/account/change_password', action: 'change_password' - match '/account/lost_password', action: 'lost_password', via: %i[get post] - match '/account/register', action: 'register', via: %i[get post patch] - get '/account/activate', action: 'activate' + scope controller: "account" do + get "/account/force_password_change", action: "force_password_change" + post "/account/change_password", action: "change_password" + match "/account/lost_password", action: "lost_password", via: %i[get post] + match "/account/register", action: "register", via: %i[get post patch] + get "/account/activate", action: "activate" - match '/login', action: 'login', as: 'signin', via: %i[get post] - get '/login/internal', action: 'internal_login', as: 'internal_signin' - get '/logout', action: 'logout', as: 'signout' + match "/login", action: "login", as: "signin", via: %i[get post] + get "/login/internal", action: "internal_login", as: "internal_signin" + get "/logout", action: "logout", as: "signout" - get '/sso', action: 'auth_source_sso_failed', as: 'sso_failure' + get "/sso", action: "auth_source_sso_failed", as: "sso_failure" - get '/login/:stage/failure', action: 'stage_failure', as: 'stage_failure' - get '/login/:stage/:secret', action: 'stage_success', as: 'stage_success' + get "/login/:stage/failure", action: "stage_failure", as: "stage_failure" + get "/login/:stage/:secret", action: "stage_success", as: "stage_success" - get '/account/consent', action: 'consent', as: 'account_consent' - get '/account/decline_consent', action: 'decline_consent', as: 'account_decline_consent' - post '/account/confirm_consent', action: 'confirm_consent', as: 'account_confirm_consent' + get "/account/consent", action: "consent", as: "account_consent" + get "/account/decline_consent", action: "decline_consent", as: "account_decline_consent" + post "/account/confirm_consent", action: "confirm_consent", as: "account_confirm_consent" end # Because of https://github.com/intridea/grape/pull/853/files this has to be @@ -102,7 +102,7 @@ # returned for all routes for which the v3 has also resources. Grape does # remove the prefix (v3) before checking whether the method is supported. I # don't understand why that should make sense. - mount API::Root => '/api' + mount API::Root => "/api" # OAuth authorization routes use_doorkeeper do @@ -110,45 +110,45 @@ skip_controllers :applications, :authorized_applications end - get '/roles/workflow/:id/:role_id/:type_id' => 'roles#workflow' + get "/roles/workflow/:id/:role_id/:type_id" => "roles#workflow" - get '/types/:id/edit/:tab' => "types#edit", + get "/types/:id/edit/:tab" => "types#edit", as: "edit_type_tab" - match '/types/:id/update/:tab' => "types#update", + match "/types/:id/update/:tab" => "types#update", as: "update_type_tab", via: %i[post patch] resources :types do - post 'move/:id', action: 'move', on: :collection + post "move/:id", action: "move", on: :collection end resources :statuses, except: :show do collection do - post 'update_work_package_done_ratio' + post "update_work_package_done_ratio" end end - get 'custom_style/:digest/logo/:filename' => 'custom_styles#logo_download', - as: 'custom_style_logo', + get "custom_style/:digest/logo/:filename" => "custom_styles#logo_download", + as: "custom_style_logo", constraints: { filename: /[^\/]*/ } - get 'custom_style/:digest/export_logo/:filename' => 'custom_styles#export_logo_download', - as: 'custom_style_export_logo', + get "custom_style/:digest/export_logo/:filename" => "custom_styles#export_logo_download", + as: "custom_style_export_logo", constraints: { filename: /[^\/]*/ } - get 'custom_style/:digest/export_cover/:filename' => 'custom_styles#export_cover_download', - as: 'custom_style_export_cover', + get "custom_style/:digest/export_cover/:filename" => "custom_styles#export_cover_download", + as: "custom_style_export_cover", constraints: { filename: /[^\/]*/ } - get 'custom_style/:digest/favicon/:filename' => 'custom_styles#favicon_download', - as: 'custom_style_favicon', + get "custom_style/:digest/favicon/:filename" => "custom_styles#favicon_download", + as: "custom_style_favicon", constraints: { filename: /[^\/]*/ } - get 'custom_style/:digest/touch-icon/:filename' => 'custom_styles#touch_icon_download', - as: 'custom_style_touch_icon', + get "custom_style/:digest/touch-icon/:filename" => "custom_styles#touch_icon_download", + as: "custom_style_touch_icon", constraints: { filename: /[^\/]*/ } - get 'highlighting/styles(/:version_tag)' => 'highlighting#styles', - as: 'highlighting_css_styles' + get "highlighting/styles(/:version_tag)" => "highlighting#styles", + as: "highlighting_css_styles" resources :custom_fields, except: :show do member do @@ -158,25 +158,25 @@ end end - get '(projects/:project_id)/search' => 'search#index', as: 'search' + get "(projects/:project_id)/search" => "search#index", as: "search" # only providing routes for journals when there are multiple subclasses of journals # all subclasses will look for the journals routes resources :journals, only: :index do - get 'diff/:field', action: :diff, on: :member, as: 'diff' + get "diff/:field", action: :diff, on: :member, as: "diff" end # REVIEW: review those wiki routes - scope 'projects/:project_id/wiki/:id' do + scope "projects/:project_id/wiki/:id" do resource :wiki_menu_item, only: %i[edit update] end # generic route for adding/removing watchers. # Models declared as acts_as_watchable will be automatically added to # OpenProject::Acts::Watchable::Routes.watched - scope ':object_type/:object_id', constraints: OpenProject::Acts::Watchable::Routes do - post '/watch' => 'watchers#watch' - delete '/unwatch' => 'watchers#unwatch' + scope ":object_type/:object_id", constraints: OpenProject::Acts::Watchable::Routes do + post "/watch" => "watchers#watch" + delete "/unwatch" => "watchers#unwatch" end namespace :projects do @@ -185,32 +185,32 @@ end resources :projects, except: %i[show edit create update] do - scope module: 'projects' do - namespace 'settings' do - resource :general, only: %i[show], controller: 'general' + scope module: "projects" do + namespace "settings" do + resource :general, only: %i[show], controller: "general" resource :modules, only: %i[show update] resource :types, only: %i[show update] resource :custom_fields, only: %i[show update] - resource :repository, only: %i[show], controller: 'repository' + resource :repository, only: %i[show], controller: "repository" resource :versions, only: %i[show] resource :categories, only: %i[show update] - resource :storage, only: %i[show], controller: 'storage' + resource :storage, only: %i[show], controller: "storage" end - resource :templated, only: %i[create destroy], controller: 'templated' - resource :archive, only: %i[create destroy], controller: 'archive' - resource :identifier, only: %i[show update], controller: 'identifier' + resource :templated, only: %i[create destroy], controller: "templated" + resource :archive, only: %i[create destroy], controller: "archive" + resource :identifier, only: %i[show update], controller: "identifier" end member do - get "settings", to: redirect('projects/%{id}/settings/general/') + get "settings", to: redirect("projects/%{id}/settings/general/") get :copy patch :types # Destroy uses a get request to prompt the user before the actual DELETE request - get :destroy_info, as: 'confirm_destroy' + get :destroy_info, as: "confirm_destroy" end resources :versions, only: %i[new create] do @@ -222,7 +222,7 @@ # this is only another name for versions#index # For nice "road in the url for the index action # this could probably be rewritten with a resource as: 'roadmap' - get '/roadmap' => 'versions#index' + get "/roadmap" => "versions#index" resources :news, only: %i[index new create] @@ -233,25 +233,25 @@ constraints: { id: /([^\/]+(?=\.markdown)|[^\/]+)/ }, except: %i[index create] do collection do - post '/new' => 'wiki#create', as: 'create' + post "/new" => "wiki#create", as: "create" get :export - get '/index' => 'wiki#index' + get "/index" => "wiki#index" get :menu end member do - get '/new' => 'wiki#new_child', as: 'new_child' - get '/diff/:version/vs/:version_from' => 'wiki#diff', as: 'wiki_diff_compare' - get '/diff(/:version)' => 'wiki#diff', as: 'wiki_diff' - get '/annotate/:version' => 'wiki#annotate', as: 'wiki_annotate' - get '/toc' => 'wiki#index' + get "/new" => "wiki#new_child", as: "new_child" + get "/diff/:version/vs/:version_from" => "wiki#diff", as: "wiki_diff_compare" + get "/diff(/:version)" => "wiki#diff", as: "wiki_diff" + get "/annotate/:version" => "wiki#annotate", as: "wiki_annotate" + get "/toc" => "wiki#index" match :rename, via: %i[get patch] - get :parent_page, action: 'edit_parent_page' - patch :parent_page, action: 'update_parent_page' + get :parent_page, action: "edit_parent_page" + patch :parent_page, action: "update_parent_page" get :history post :protect - get :select_main_menu_item, to: 'wiki_menu_items#select_main_menu_item' - post :replace_main_menu_item, to: 'wiki_menu_items#replace_main_menu_item' + get :select_main_menu_item, to: "wiki_menu_items#select_main_menu_item" + post :replace_main_menu_item, to: "wiki_menu_items#replace_main_menu_item" get :menu end end @@ -260,24 +260,24 @@ # it is necessary to define the show action later # than any other route as it otherwise would # work as a catchall for everything under /wiki - get 'wiki' => 'wiki#show' + get "wiki" => "wiki#show" resources :work_packages, only: [] do collection do - get '/report/:detail' => 'work_packages/reports#report_details' - get '/report' => 'work_packages/reports#report' + get "/report/:detail" => "work_packages/reports#report_details" + get "/report" => "work_packages/reports#report" end # states managed by client-side routing on work_package#index - get '(/*state)' => 'work_packages#index', on: :collection, as: '' - get '/create_new' => 'work_packages#index', on: :collection, as: 'new_split' - get '/new' => 'work_packages#index', on: :collection, as: 'new' + get "(/*state)" => "work_packages#index", on: :collection, as: "" + get "/create_new" => "work_packages#index", on: :collection, as: "new_split" + get "/new" => "work_packages#index", on: :collection, as: "new" # state for show view in project context - get '(/*state)' => 'work_packages#show', on: :member, as: '' + get "(/*state)" => "work_packages#show", on: :member, as: "" end - resources :activity, :activities, only: :index, controller: 'activities' do + resources :activity, :activities, only: :index, controller: "activities" do collection do get :menu end @@ -303,7 +303,7 @@ resource :menu, only: %[show] end - resource :repository, controller: 'repositories', except: [:new] do + resource :repository, controller: "repositories", except: [:new] do # Destroy uses a get request to prompt the user before the actual DELETE request get :destroy_info get :committers @@ -311,36 +311,36 @@ get :graph get :revisions - get '/statistics', action: :stats, as: 'stats' + get "/statistics", action: :stats, as: "stats" - get '(/revisions/:rev)/diff.:format', action: :diff - get '(/revisions/:rev)/diff(/*repo_path)', + get "(/revisions/:rev)/diff.:format", action: :diff + get "(/revisions/:rev)/diff(/*repo_path)", action: :diff, - format: 'html', - constraints: { rev: /[\w0-9.\-_]+/, repo_path: /.*/ } + format: "html", + constraints: { rev: /[\w.\-]+/, repo_path: /.*/ } - get '(/revisions/:rev)/:format/*repo_path', + get "(/revisions/:rev)/:format/*repo_path", action: :entry, format: /raw/, - rev: /[\w0-9.\-_]+/ + rev: /[\w.\-]+/ %w{diff annotate changes entry browse}.each do |action| get "(/revisions/:rev)/#{action}(/*repo_path)", - format: 'html', + format: "html", action:, - constraints: { rev: /[\w0-9.\-_]+/, repo_path: /.*/ }, + constraints: { rev: /[\w.\-]+/, repo_path: /.*/ }, as: "#{action}_revision" end - get '/revision(/:rev)', rev: /[\w0-9.\-_]+/, + get "/revision(/:rev)", rev: /[\w.\-]+/, action: :revision, - as: 'show_revision' + as: "show_revision" - get '(/revisions/:rev)(/*repo_path)', + get "(/revisions/:rev)(/*repo_path)", action: :show, - format: 'html', - constraints: { rev: /[\w0-9.\-_]+/, repo_path: /.*/ }, - as: 'show_revisions_path' + format: "html", + constraints: { rev: /[\w.\-]+/, repo_path: /.*/ }, + as: "show_revisions_path" end end @@ -352,52 +352,52 @@ end end - scope 'admin' do + scope "admin" do resource :announcements, only: %i[edit update] constraints(Constraints::Enterprise) do resource :enterprise, only: %i[show create destroy] - scope controller: 'enterprises' do - post 'enterprise/save_trial_key' => 'enterprises#save_trial_key' - delete 'enterprise/delete_trial_key' => 'enterprises#delete_trial_key' + scope controller: "enterprises" do + post "enterprise/save_trial_key" => "enterprises#save_trial_key" + delete "enterprise/delete_trial_key" => "enterprises#delete_trial_key" end end resources :enumerations do - post 'move/:id', action: 'move', on: :collection + post "move/:id", action: "move", on: :collection end - delete 'design/logo' => 'custom_styles#logo_delete', as: 'custom_style_logo_delete' - delete 'design/export_logo' => 'custom_styles#export_logo_delete', as: 'custom_style_export_logo_delete' - delete 'design/export_cover' => 'custom_styles#export_cover_delete', as: 'custom_style_export_cover_delete' - delete 'design/favicon' => 'custom_styles#favicon_delete', as: 'custom_style_favicon_delete' - delete 'design/touch_icon' => 'custom_styles#touch_icon_delete', as: 'custom_style_touch_icon_delete' - get 'design/upsale' => 'custom_styles#upsale', as: 'custom_style_upsale' - post 'design/colors' => 'custom_styles#update_colors', as: 'update_design_colors' - post 'design/themes' => 'custom_styles#update_themes', as: 'update_design_themes' - post 'design/export_cover_text_color' => 'custom_styles#update_export_cover_text_color', - as: 'update_custom_style_export_cover_text_color' + delete "design/logo" => "custom_styles#logo_delete", as: "custom_style_logo_delete" + delete "design/export_logo" => "custom_styles#export_logo_delete", as: "custom_style_export_logo_delete" + delete "design/export_cover" => "custom_styles#export_cover_delete", as: "custom_style_export_cover_delete" + delete "design/favicon" => "custom_styles#favicon_delete", as: "custom_style_favicon_delete" + delete "design/touch_icon" => "custom_styles#touch_icon_delete", as: "custom_style_touch_icon_delete" + get "design/upsale" => "custom_styles#upsale", as: "custom_style_upsale" + post "design/colors" => "custom_styles#update_colors", as: "update_design_colors" + post "design/themes" => "custom_styles#update_themes", as: "update_design_themes" + post "design/export_cover_text_color" => "custom_styles#update_export_cover_text_color", + as: "update_custom_style_export_cover_text_color" - resource :custom_style, only: %i[update show create], path: 'design' + resource :custom_style, only: %i[update show create], path: "design" resources :attribute_help_texts, only: %i(index new create edit update destroy) do - get :upsale, to: 'attribute_help_texts#upsale', on: :collection, as: :upsale + get :upsale, to: "attribute_help_texts#upsale", on: :collection, as: :upsale end resources :groups, except: %i[show] do member do # this should be put into it's own resource - post '/members' => 'groups#add_users', as: 'members_of' - delete '/members/:user_id' => 'groups#remove_user', as: 'member_of' + post "/members" => "groups#add_users", as: "members_of" + delete "/members/:user_id" => "groups#remove_user", as: "member_of" # this should be put into it's own resource - patch '/memberships/:membership_id' => 'groups#edit_membership', as: 'membership_of' - put '/memberships/:membership_id' => 'groups#edit_membership' - delete '/memberships/:membership_id' => 'groups#destroy_membership' - post '/memberships' => 'groups#create_memberships', as: 'memberships_of' + patch "/memberships/:membership_id" => "groups#edit_membership", as: "membership_of" + put "/memberships/:membership_id" => "groups#edit_membership" + delete "/memberships/:membership_id" => "groups#destroy_membership" + post "/memberships" => "groups#create_memberships", as: "memberships_of" end end resources :roles, except: %i[show] do collection do - put '/' => 'roles#bulk_update' + put "/" => "roles#bulk_update" get :report end end @@ -422,37 +422,37 @@ patch tab[:name], controller: tab[:controller], action: :update, as: "update_#{tab[:name]}" end - resource :authentication, controller: '/admin/settings/authentication_settings', only: %i[show update] - resource :attachments, controller: '/admin/settings/attachments_settings', only: %i[show update] - resource :virus_scanning, controller: '/admin/settings/virus_scanning_settings', only: %i[show update] do + resource :authentication, controller: "/admin/settings/authentication_settings", only: %i[show update] + resource :attachments, controller: "/admin/settings/attachments_settings", only: %i[show update] + resource :virus_scanning, controller: "/admin/settings/virus_scanning_settings", only: %i[show update] do collection do get :av_form end end - resource :incoming_mails, controller: '/admin/settings/incoming_mails_settings', only: %i[show update] - resource :aggregation, controller: '/admin/settings/aggregation_settings', only: %i[show update] - resource :mail_notifications, controller: '/admin/settings/mail_notifications_settings', only: %i[show update] - resource :api, controller: '/admin/settings/api_settings', only: %i[show update] - resource :work_packages, controller: '/admin/settings/work_packages_settings', only: %i[show update] - resource :working_days, controller: '/admin/settings/working_days_settings', only: %i[show update] - resource :users, controller: '/admin/settings/users_settings', only: %i[show update] - resource :date_format, controller: '/admin/settings/date_format_settings', only: %i[show update] - resource :icalendar, controller: '/admin/settings/icalendar_settings', only: %i[show update] + resource :incoming_mails, controller: "/admin/settings/incoming_mails_settings", only: %i[show update] + resource :aggregation, controller: "/admin/settings/aggregation_settings", only: %i[show update] + resource :mail_notifications, controller: "/admin/settings/mail_notifications_settings", only: %i[show update] + resource :api, controller: "/admin/settings/api_settings", only: %i[show update] + resource :work_packages, controller: "/admin/settings/work_packages_settings", only: %i[show update] + resource :working_days, controller: "/admin/settings/working_days_settings", only: %i[show update] + resource :users, controller: "/admin/settings/users_settings", only: %i[show update] + resource :date_format, controller: "/admin/settings/date_format_settings", only: %i[show update] + resource :icalendar, controller: "/admin/settings/icalendar_settings", only: %i[show update] # Redirect /settings to general settings - get '/', to: redirect('/admin/settings/general') + get "/", to: redirect("/admin/settings/general") # Plugin settings - get 'plugin/:id', action: :show_plugin - post 'plugin/:id', action: :update_plugin + get "plugin/:id", action: :show_plugin + post "plugin/:id", action: :update_plugin end resources :quarantined_attachments, - controller: '/admin/attachments/quarantined_attachments', + controller: "/admin/attachments/quarantined_attachments", only: %i[index destroy] - resource :backups, controller: '/admin/backups', only: %i[show] do + resource :backups, controller: "/admin/backups", only: %i[show] do collection do get :reset_token post :reset_token, action: :perform_token_reset @@ -465,48 +465,48 @@ resource :workflows, only: %i[edit update show] do member do # We should fix this crappy routing (split up and rename controller methods) - match 'copy', action: 'copy', via: %i[get post] + match "copy", action: "copy", via: %i[get post] end end namespace :work_packages do - match 'auto_complete' => 'auto_completes#index', via: %i[get post] - resource :bulk, controller: 'bulk', only: %i[edit update destroy] + match "auto_complete" => "auto_completes#index", via: %i[get post] + resource :bulk, controller: "bulk", only: %i[edit update destroy] # FIXME: this is kind of evil!! We need to remove this soonest and # cover the functionality. Route is being used in work-package-service.js:331 - get '/bulk' => 'bulk#destroy' + get "/bulk" => "bulk#destroy" resources :shares, only: %i[destroy update] end resources :work_packages, only: [:index] do # move bulk of wps - get 'move/new' => 'work_packages/moves#new', on: :collection, as: 'new_move' - post 'move' => 'work_packages/moves#create', on: :collection, as: 'move' + get "move/new" => "work_packages/moves#new", on: :collection, as: "new_move" + post "move" => "work_packages/moves#create", on: :collection, as: "move" # move individual wp - resource :move, controller: 'work_packages/moves', only: %i[new create] + resource :move, controller: "work_packages/moves", only: %i[new create] # states managed by client-side routing on work_package#index - get 'details/*state' => 'work_packages#index', on: :collection, as: :details + get "details/*state" => "work_packages#index", on: :collection, as: :details # Rails managed sharing route - resources :shares, controller: 'work_packages/shares', only: %i[index create] do + resources :shares, controller: "work_packages/shares", only: %i[index create] do member do - post 'resend_invite' => 'work_packages/shares#resend_invite' + post "resend_invite" => "work_packages/shares#resend_invite" end collection do - resource :bulk, controller: 'work_packages/shares/bulk', only: %i[update destroy], as: :shares_bulk + resource :bulk, controller: "work_packages/shares/bulk", only: %i[update destroy], as: :shares_bulk end end # states managed by client-side (angular) routing on work_package#show - get '/' => 'work_packages#index', on: :collection, as: 'index' - get '/create_new' => 'work_packages#index', on: :collection, as: 'new_split' - get '/new' => 'work_packages#index', on: :collection, as: 'new', state: 'new' + get "/" => "work_packages#index", on: :collection, as: "index" + get "/create_new" => "work_packages#index", on: :collection, as: "new_split" + get "/new" => "work_packages#index", on: :collection, as: "new", state: "new" # We do not want to match the work package export routes - get '(/*state)' => 'work_packages#show', on: :member, as: '', constraints: { id: /\d+/ } - get '/share_upsale' => 'work_packages#index', on: :collection, as: 'share_upsale' - get '/edit' => 'work_packages#show', on: :member, as: 'edit' + get "(/*state)" => "work_packages#show", on: :member, as: "", constraints: { id: /\d+/ } + get "/share_upsale" => "work_packages#index", on: :collection, as: "share_upsale" + get "/edit" => "work_packages#show", on: :member, as: "edit" end resources :versions, only: %i[show edit update destroy] do @@ -515,18 +515,18 @@ end end - resources :activity, :activities, only: :index, controller: 'activities' do + resources :activity, :activities, only: :index, controller: "activities" do collection do get :menu end end resources :users, constraints: { id: /(\d+|me)/ }, except: :edit do - resources :memberships, controller: 'users/memberships', only: %i[update create destroy] + resources :memberships, controller: "users/memberships", only: %i[update create destroy] member do - get '/edit(/:tab)' => 'users#edit', as: 'edit' - get '/change_status/:change_action' => 'users#change_status_info', as: 'change_status_info' + get "/edit(/:tab)" => "users#edit", as: "edit" + get "/change_status/:change_action" => "users#change_status_info", as: "change_status_info" post :change_status post :resend_invitation get :deletion_info @@ -534,10 +534,10 @@ end resources :placeholder_users, except: :edit do - resources :memberships, controller: 'placeholder_users/memberships', only: %i[update create destroy] + resources :memberships, controller: "placeholder_users/memberships", only: %i[update create destroy] member do - get '/edit(/:tab)' => 'placeholder_users#edit', as: 'edit' + get "/edit(/:tab)" => "placeholder_users#edit", as: "edit" get :deletion_info end end @@ -546,33 +546,33 @@ resources :groups, only: %i[show], as: :show_group resources :forums, only: [] do - resources :topics, controller: 'messages', except: [:index], shallow: true do + resources :topics, controller: "messages", except: [:index], shallow: true do member do get :quote - post :reply, as: 'reply_to' + post :reply, as: "reply_to" end end end resources :news, only: %i[index destroy update edit show] do - resources :comments, controller: 'news/comments', only: %i[create destroy], shallow: true + resources :comments, controller: "news/comments", only: %i[create destroy], shallow: true end # redirect for backwards compatibility - scope 'attachments', + scope "attachments", constraints: { id: /\d+/, filename: /[^\/]*/ }, format: false do - get '/download/:id/:filename', + get "/download/:id/:filename", to: redirect("#{rails_relative_url_root}/attachments/%{id}/%{filename}") - get '/download/:id', + get "/download/:id", to: redirect("#{rails_relative_url_root}/attachments/%{id}") - scope ':id' do - get '(/:filename)', + scope ":id" do + get "(/:filename)", to: redirect("#{rails_relative_url_root}/api/v3/attachments/%{id}/content") - delete '', + delete "", to: redirect("#{rails_relative_url_root}/api/v3/attachments/%{id}") end end @@ -584,45 +584,45 @@ end end - scope controller: 'sys' do - match '/sys/repo_auth', action: 'repo_auth', via: %i[get post] - get '/sys/projects', action: 'projects' - get '/sys/fetch_changesets', action: 'fetch_changesets' - get '/sys/projects/:id/repository/update_storage', action: 'update_required_storage' + scope controller: "sys" do + match "/sys/repo_auth", action: "repo_auth", via: %i[get post] + get "/sys/projects", action: "projects" + get "/sys/fetch_changesets", action: "fetch_changesets" + get "/sys/projects/:id/repository/update_storage", action: "update_required_storage" end # alternate routes for the current user - scope 'my' do - get '/deletion_info' => 'users#deletion_info', as: 'delete_my_account_info' - post '/oauth/revoke_application/:application_id' => 'oauth/grants#revoke_application', as: 'revoke_my_oauth_application' - delete '/storage_token/:id' => 'my#delete_storage_token', as: 'storage_token_delete' + scope "my" do + get "/deletion_info" => "users#deletion_info", as: "delete_my_account_info" + post "/oauth/revoke_application/:application_id" => "oauth/grants#revoke_application", as: "revoke_my_oauth_application" + delete "/storage_token/:id" => "my#delete_storage_token", as: "storage_token_delete" - resources :sessions, controller: 'my/sessions', as: 'my_sessions', only: %i[index show destroy] - resources :auto_login_tokens, controller: 'my/auto_login_tokens', as: 'my_auto_login_tokens', only: %i[destroy] + resources :sessions, controller: "my/sessions", as: "my_sessions", only: %i[index show destroy] + resources :auto_login_tokens, controller: "my/auto_login_tokens", as: "my_auto_login_tokens", only: %i[destroy] end - scope controller: 'my' do - get '/my/password', action: 'password' - post '/my/change_password', action: 'change_password' + scope controller: "my" do + get "/my/password", action: "password" + post "/my/change_password", action: "change_password" - get '/my/account', action: 'account' - get '/my/settings', action: 'settings' - get '/my/notifications', action: 'notifications' - get '/my/reminders', action: 'reminders' + get "/my/account", action: "account" + get "/my/settings", action: "settings" + get "/my/notifications", action: "notifications" + get "/my/reminders", action: "reminders" - patch '/my/account', action: 'update_account' - patch '/my/settings', action: 'update_settings' + patch "/my/account", action: "update_account" + patch "/my/settings", action: "update_settings" - post '/my/generate_rss_key', action: 'generate_rss_key' - delete '/my/revoke_rss_key', action: 'revoke_rss_key' - post '/my/generate_api_key', action: 'generate_api_key' - delete '/my/revoke_api_key', action: 'revoke_api_key' - delete '/my/revoke_ical_token', action: 'revoke_ical_token' - get '/my/access_token', action: 'access_token' + post "/my/generate_rss_key", action: "generate_rss_key" + delete "/my/revoke_rss_key", action: "revoke_rss_key" + post "/my/generate_api_key", action: "generate_api_key" + delete "/my/revoke_api_key", action: "revoke_api_key" + delete "/my/revoke_ical_token", action: "revoke_ical_token" + get "/my/access_token", action: "access_token" end - scope controller: 'onboarding' do - patch 'user_settings', action: 'user_settings' + scope controller: "onboarding" do + patch "user_settings", action: "user_settings" end resources :colors do @@ -633,18 +633,18 @@ end end - get '/robots' => 'homescreen#robots', defaults: { format: :txt } + get "/robots" => "homescreen#robots", defaults: { format: :txt } - root to: 'account#login' + root to: "account#login" scope :notifications do - get '(/*state)', to: 'angular#notifications_layout', as: :notifications_center + get "(/*state)", to: "angular#notifications_layout", as: :notifications_center end # OAuthClient needs a "callback" URL that Nextcloud calls with a "code" (see OAuth2 RFC) - scope 'oauth_clients/:oauth_client_id' do - get 'callback', controller: 'oauth_clients', action: :callback - get 'ensure_connection', controller: 'oauth_clients', action: :ensure_connection, as: 'oauth_clients_ensure_connection' + scope "oauth_clients/:oauth_client_id" do + get "callback", controller: "oauth_clients", action: :callback + get "ensure_connection", controller: "oauth_clients", action: :ensure_connection, as: "oauth_clients_ensure_connection" end if OpenProject::Configuration.lookbook_enabled? @@ -652,6 +652,6 @@ end if Rails.env.development? - mount GoodJob::Engine => 'good_job' + mount GoodJob::Engine => "good_job" end end diff --git a/docs/getting-started/my-account/README.md b/docs/getting-started/my-account/README.md index a47a2ae730cf..59d704e31575 100644 --- a/docs/getting-started/my-account/README.md +++ b/docs/getting-started/my-account/README.md @@ -116,7 +116,7 @@ In order to register a new device for two-factor authentication, click the green - Mobile phone - App-based authenticator -- WebAuth +- WebAuthn ![](openproject_my_account_authentication_options.png) diff --git a/docs/user-guide/projects/project-settings/file-storages/README.md b/docs/user-guide/projects/project-settings/file-storages/README.md index 189f945c6680..e5dc87166037 100644 --- a/docs/user-guide/projects/project-settings/file-storages/README.md +++ b/docs/user-guide/projects/project-settings/file-storages/README.md @@ -60,6 +60,8 @@ The SharePoint file storage is now available to all work packages in this projec > **Note:** Please refer to the [OneDrive/SharePoint user guide](../../../file-management/one-drive-integration) for further instructions on using the integration at a user level. + + ## Add a Nextcloud storage to a project If you have selected the Nextcloud option in the previous step of storage selection, you will now see the **Project folder** options. @@ -80,7 +82,11 @@ Click on **Add** to add your new Nextcloud file storage to this project. The Nextcloud file storage is now available to all work packages in this project. -> **Note:** For information on how to use the file storage (link Nextcloud user accounts at a user level, link files to a work package, view and download linked files, unlink files), please read our [Nextcloud integration user guide](../../../file-management/nextcloud-integration/). +> **Note:** For information on how to use the file storage (link Nextcloud user accounts at a user level, link files to a work package, view and download linked files, unlink files), please read our [Nextcloud integration user guide](../../../file-management/nextcloud-integration/). + +If you do not yet have an access token for the file storage in a project, you will be prompted to log into your file storage. You can choose to login immediately to establish the connection or to do it later. + +![OAuth for file storages in OpenProject project settings](file-storages-oauth-nudge-nextcloud.png) ### Project folder member connection status diff --git a/docs/user-guide/projects/project-settings/file-storages/file-storages-oauth-nudge-nextcloud.png b/docs/user-guide/projects/project-settings/file-storages/file-storages-oauth-nudge-nextcloud.png new file mode 100644 index 000000000000..80a6224cb596 Binary files /dev/null and b/docs/user-guide/projects/project-settings/file-storages/file-storages-oauth-nudge-nextcloud.png differ diff --git a/lib/open_project/static/links.rb b/lib/open_project/static/links.rb index 18f38ad5c337..c89d47ff0ec8 100644 --- a/lib/open_project/static/links.rb +++ b/lib/open_project/static/links.rb @@ -44,6 +44,10 @@ def links @links ||= static_links.merge(dynamic_links) end + def url_for(item) + links.dig(item, :href) + end + def has?(name) @links.key? name end @@ -277,6 +281,9 @@ def static_links }, ical_docs: { href: 'https://www.openproject.org/docs/user-guide/calendar/#subscribe-to-a-calendar' + }, + integrations: { + href: 'https://www.openproject.org/docs/system-admin-guide/integrations/' } } end diff --git a/lib/open_project/version.rb b/lib/open_project/version.rb index 69c9ca44612d..b545805e3995 100644 --- a/lib/open_project/version.rb +++ b/lib/open_project/version.rb @@ -26,8 +26,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require 'rexml/document' -require 'open3' +require "rexml/document" +require "open3" module OpenProject module VERSION # :nodoc: @@ -48,7 +48,7 @@ class << self # # 2.0.0debian-2 def special - '' + "" end def revision @@ -57,31 +57,31 @@ def revision def core_sha cached_or_block(:@core_sha) do - read_optional 'CORE_VERSION' + read_optional "CORE_VERSION" end end def core_url cached_or_block(:@core_url) do - read_optional 'CORE_URL' + read_optional "CORE_URL" end end def product_sha cached_or_block(:@product_sha) do - read_optional 'PRODUCT_VERSION' + read_optional "PRODUCT_VERSION" end end def product_url cached_or_block(:@product_url) do - read_optional 'PRODUCT_URL' + read_optional "PRODUCT_URL" end end def builder_sha cached_or_block(:@builder_sha) do - read_optional 'BUILDER_VERSION' + read_optional "BUILDER_VERSION" end end @@ -98,14 +98,14 @@ def to_a; ARRAY end def to_s; STRING end def to_semver - [MAJOR, MINOR, PATCH].join('.') + special + [MAJOR, MINOR, PATCH].join(".") + special end private def release_date_from_file cached_or_block(:@release_date_from_file) do - path = Rails.root.join('RELEASE_DATE') + path = Rails.root.join("RELEASE_DATE") if File.exist? path s = File.read(path) Date.parse(s) @@ -115,7 +115,7 @@ def release_date_from_file def release_date_from_git cached_or_block(:@release_date_from_git) do - date, = Open3.capture3('git', 'log', '-1', '--format=%cd', '--date=short') + date, = Open3.capture3("git", "log", "-1", "--format=%cd", "--date=short") Date.parse(date) if date end end @@ -128,7 +128,7 @@ def revision_from_core_sha def revision_from_git cached_or_block(:@revision) do - revision, = Open3.capture3('git', 'rev-parse', 'HEAD') + revision, = Open3.capture3("git", "rev-parse", "HEAD") if revision.present? revision.strip[0..8] end @@ -157,6 +157,6 @@ def cached_or_block(variable) REVISION = revision ARRAY = [MAJOR, MINOR, PATCH, REVISION].compact - STRING = ARRAY.join('.') + STRING = ARRAY.join(".") end end diff --git a/lib/redmine/plugin.rb b/lib/redmine/plugin.rb index 4769847a8b6b..af0cac68c5ae 100644 --- a/lib/redmine/plugin.rb +++ b/lib/redmine/plugin.rb @@ -96,6 +96,7 @@ def def_field(*names) end end def_field :gem_name, :url, :author, :author_url, :version, :settings, :bundled + alias :bundled? :bundled attr_reader :id # Plugin constructor @@ -165,6 +166,12 @@ def self.all registered_plugins.values end + def self.not_bundled + registered_plugins + .values + .reject(&:bundled) + end + # Finds a plugin by its id # Returns a PluginNotFound exception if the plugin doesn't exist def self.find(id) diff --git a/modules/bim/lib/open_project/bim/engine.rb b/modules/bim/lib/open_project/bim/engine.rb index 741aac4738b7..3a9bd2ed922b 100644 --- a/modules/bim/lib/open_project/bim/engine.rb +++ b/modules/bim/lib/open_project/bim/engine.rb @@ -36,6 +36,7 @@ class Engine < ::Rails::Engine register 'openproject-bim', author_url: 'https://www.openproject.org', + bundled: true, settings: { default: {} } do diff --git a/modules/storages/app/forms/storages/admin/submit_or_cancel_form.rb b/modules/storages/app/forms/storages/admin/submit_or_cancel_form.rb index e6a8b227aca0..064961e92fc4 100644 --- a/modules/storages/app/forms/storages/admin/submit_or_cancel_form.rb +++ b/modules/storages/app/forms/storages/admin/submit_or_cancel_form.rb @@ -58,7 +58,7 @@ def default_cancel_button_options name: :cancel, scheme: :default, tag: :a, - href: Rails.application.routes.url_helpers.admin_settings_storages_path, + href: OpenProject::StaticRouting::StaticRouter.new.url_helpers.admin_settings_storages_path, label: I18n.t('button_cancel') } end diff --git a/modules/storages/app/models/storages/project_storage.rb b/modules/storages/app/models/storages/project_storage.rb index 3ae6dcb71e4e..8e9563377f06 100644 --- a/modules/storages/app/models/storages/project_storage.rb +++ b/modules/storages/app/models/storages/project_storage.rb @@ -92,7 +92,7 @@ def open(user) def open_with_connection_ensured return unless storage.configured? - url_helpers = Rails.application.routes.url_helpers + url_helpers = OpenProject::StaticRouting::StaticRouter.new.url_helpers open_project_storage_url = url_helpers.open_project_storage_url( host: Setting.host_name, protocol: 'https', diff --git a/modules/storages/spec/features/storages_menu_links_spec.rb b/modules/storages/spec/features/storages_menu_links_spec.rb index 969b12c2ad73..63d4347af018 100644 --- a/modules/storages/spec/features/storages_menu_links_spec.rb +++ b/modules/storages/spec/features/storages_menu_links_spec.rb @@ -31,23 +31,25 @@ require 'spec_helper' require_module_spec_helper -RSpec.describe 'Storage links in project menu', :js, :with_cuprite do +RSpec.describe 'Storage links in project menu' do include EnsureConnectionPathHelper - let!(:storage_configured_linked1) { create(:nextcloud_storage_configured, :as_automatically_managed, name: "Storage 1") } - let!(:project_storage1) { create(:project_storage, :as_automatically_managed, project:, storage: storage_configured_linked1) } - let!(:storage_configured_linked2) { create(:nextcloud_storage_configured, name: "Storage 2") } - let!(:project_storage2) do + shared_let(:project) { create(:project, enabled_module_names: %i[storages]) } + shared_let(:storage_configured_linked1) { create(:nextcloud_storage_configured, :as_automatically_managed, name: "Storage 1") } + shared_let(:project_storage1) do + create(:project_storage, :as_automatically_managed, project:, storage: storage_configured_linked1) + end + shared_let(:storage_configured_linked2) { create(:nextcloud_storage_configured, name: "Storage 2") } + shared_let(:project_storage2) do create(:project_storage, project_folder_mode: 'inactive', project:, storage: storage_configured_linked2) end - let!(:storage_configured_linked3) { create(:nextcloud_storage_configured, name: "Storage 3") } - let!(:project_storage3) do + shared_let(:storage_configured_linked3) { create(:nextcloud_storage_configured, name: "Storage 3") } + shared_let(:project_storage3) do create(:project_storage, project_folder_mode: 'manual', project:, storage: storage_configured_linked3) end - let!(:storage_configured_unlinked) { create(:nextcloud_storage_configured, name: "Storage 4") } - let!(:storage_unconfigured_linked) { create(:nextcloud_storage, name: "Storage 5") } - let!(:project_storage4) { create(:project_storage, project:, storage: storage_unconfigured_linked) } - let!(:project) { create(:project, enabled_module_names: %i[storages]) } + shared_let(:storage_configured_unlinked) { create(:nextcloud_storage_configured, name: "Storage 4") } + shared_let(:storage_unconfigured_linked) { create(:nextcloud_storage, name: "Storage 5") } + shared_let(:project_storage4) { create(:project_storage, project:, storage: storage_unconfigured_linked) } let(:user) { create(:user, member_with_permissions: { project => permissions }) } before do @@ -82,6 +84,22 @@ expect(page).to have_no_link(storage_configured_unlinked.name) expect(page).to have_no_link(storage_unconfigured_linked.name) end + + context 'when OP has been installed behind prefix' do + let(:prefix) { '/qwerty' } + + before { allow(OpenProject::Configuration).to receive(:rails_relative_url_root).and_return(prefix) } + + it 'has all links prefixed' do + visit(project_path(id: project.id)) + + expect(page).to have_link(storage_configured_linked1.name, href: ensure_connection_path(project_storage1)) + expect(page).to have_link(storage_configured_linked2.name, href: ensure_connection_path(project_storage2)) + expect(page).to have_link(storage_configured_linked3.name, href: ensure_connection_path(project_storage3)) + expect(page).to have_no_link(storage_configured_unlinked.name) + expect(page).to have_no_link(storage_unconfigured_linked.name) + end + end end context 'read_files' do diff --git a/modules/storages/spec/support/ensure_connection_path_helper.rb b/modules/storages/spec/support/ensure_connection_path_helper.rb index 82d621094f03..58d1baecda45 100644 --- a/modules/storages/spec/support/ensure_connection_path_helper.rb +++ b/modules/storages/spec/support/ensure_connection_path_helper.rb @@ -30,10 +30,11 @@ module EnsureConnectionPathHelper def ensure_connection_path(project_storage) - oauth_clients_ensure_connection_path( + url_helpers = OpenProject::StaticRouting::StaticRouter.new.url_helpers + url_helpers.oauth_clients_ensure_connection_path( oauth_client_id: project_storage.storage.oauth_client.client_id, storage_id: project_storage.storage.id, - destination_url: open_project_storage_url( + destination_url: url_helpers.open_project_storage_url( protocol: 'https', project_id: project_storage.project.identifier, id: project_storage.id diff --git a/modules/two_factor_authentication/app/controllers/concerns/two_factor_authentication/webauthn_relying_party.rb b/modules/two_factor_authentication/app/controllers/concerns/two_factor_authentication/webauthn_relying_party.rb new file mode 100644 index 000000000000..8fcde41b375c --- /dev/null +++ b/modules/two_factor_authentication/app/controllers/concerns/two_factor_authentication/webauthn_relying_party.rb @@ -0,0 +1,19 @@ +module ::TwoFactorAuthentication + module WebauthnRelyingParty + extend ActiveSupport::Concern + + protected + + def webauthn_relying_party + @webauthn_relying_party ||= begin + origin = "#{Setting.protocol}://#{Setting.host_name}" + + WebAuthn::RelyingParty.new( + origin:, + id: URI(origin).host, + name: Setting.app_title + ) + end + end + end +end diff --git a/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb b/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb index d5d82c6266c8..a16a2a070608 100644 --- a/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb +++ b/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb @@ -4,8 +4,10 @@ class AuthenticationController < ApplicationController include ::TwoFactorAuthentication::RememberToken # Backup tokens functionality include ::TwoFactorAuthentication::BackupCodes + # Webauthn relying party based on domain + include ::TwoFactorAuthentication::WebauthnRelyingParty # Include global layout helper - layout 'no_menu' + layout "no_menu" # User is not yet logged in, so skip login required check skip_before_action :check_if_login_required @@ -29,7 +31,7 @@ def request_otp session[:authenticated_user_force_2fa] = service.needs_registration? if service.needs_registration? - flash[:info] = I18n.t('two_factor_authentication.forced_registration.required_to_add_device') + flash[:info] = I18n.t("two_factor_authentication.forced_registration.required_to_add_device") redirect_to new_forced_2fa_device_path elsif !service.requires_token? complete_stage_redirect @@ -82,7 +84,7 @@ def successful_2fa_transmission(service, transmit) ## # Create a token service for the current user # with an optional override to use a non-default channel - def otp_service(user, use_channel: nil, use_device: nil) + def otp_service(user, use_channel: nil, use_device: remembered_device(user)) session[:two_factor_authentication_device_id] = use_device.try(:id) ::TwoFactorAuthentication::TokenService.new user:, use_channel:, use_device: end @@ -90,16 +92,18 @@ def otp_service(user, use_channel: nil, use_device: nil) ## # Get the used device for verification def otp_service_for_verification(user) - use_device = - if session[:two_factor_authentication_device_id] - user.otp_devices.find(session[:two_factor_authentication_device_id]) - end - otp_service(user, use_device:) + otp_service(user, use_device: remembered_device(user)) rescue ActiveRecord::RecordNotFound render_404 false end + def remembered_device(user) + if session[:two_factor_authentication_device_id] + user.otp_devices.find(session[:two_factor_authentication_device_id]) + end + end + ## # Detect overridden channel or device from params when trying to resend def service_from_resend_params @@ -142,9 +146,9 @@ def render_login_otp(service) @active_devices = @user.otp_devices.get_active if params["back_url"] - render action: 'request_otp', back_url: params["back_url"] + render action: "request_otp", back_url: params["back_url"] else - render action: 'request_otp' + render action: "request_otp" end end @@ -206,7 +210,7 @@ def manager # In case of mis-configuration, block all logins def ensure_valid_configuration if manager.invalid_configuration? - render_500 message: I18n.t('two_factor_authentication.error_is_enforced_not_active') + render_500 message: I18n.t("two_factor_authentication.error_is_enforced_not_active") false end end @@ -221,12 +225,5 @@ def complete_stage_redirect def failure_stage_redirect redirect_to authentication_stage_failure_path :two_factor_authentication end - - def webauthn_relying_party - @webauthn_relying_party ||= WebAuthn::RelyingParty.new( - origin: "#{Setting.protocol}://#{Setting.host_name}", - name: Setting.app_title - ) - end end end diff --git a/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb b/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb index 0f9bfb8bf995..bf5ca78a4af8 100644 --- a/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb +++ b/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb @@ -1,5 +1,7 @@ module ::TwoFactorAuthentication class BaseController < ApplicationController + include ::TwoFactorAuthentication::WebauthnRelyingParty + # Ensure 2FA authentication is enabled before_action :ensure_enabled_2fa @@ -10,17 +12,17 @@ class BaseController < ApplicationController helper_method :optional_webauthn_challenge_url - layout 'no_menu' + layout "no_menu" def new if params[:type] @device_type = params[:type].to_sym @device = new_device_type! @device_type - render 'two_factor_authentication/two_factor_devices/new' + render "two_factor_authentication/two_factor_devices/new" else @available_devices = available_devices - render 'two_factor_authentication/two_factor_devices/new_type' + render "two_factor_authentication/two_factor_devices/new_type" end end @@ -32,7 +34,7 @@ def make_default if @device.make_default! flash[:notice] = t(:notice_successful_update) else - flash[:error] = t('two_factor_authentication.devices.make_default_failed') + flash[:error] = t("two_factor_authentication.devices.make_default_failed") end redirect_to index_path @@ -42,14 +44,14 @@ def make_default # Destroy the given device if its not the default def destroy if @device.default && strategy_manager.enforced? - render_400 message: t('two_factor_authentication.devices.is_default_cannot_delete') + render_400 message: t("two_factor_authentication.devices.is_default_cannot_delete") return end if @device.destroy flash[:notice] = t(:notice_successful_delete) else - flash[:error] = t('two_factor_authentication.devices.failed_to_delete') + flash[:error] = t("two_factor_authentication.devices.failed_to_delete") Rails.logger.error "Failed to delete #{@device.id} of user#{target_user.id}. Errors: #{@device.errors.full_messages.join(' ')}" end @@ -89,8 +91,8 @@ def request_device_confirmation_token request_token_for_device( @device, confirm_path: url_for(action: :confirm, device_id: @device.id), - title: I18n.t('two_factor_authentication.devices.confirm_device'), - message: I18n.t('two_factor_authentication.devices.text_confirm_to_complete_html', identifier: @device.identifier) + title: I18n.t("two_factor_authentication.devices.confirm_device"), + message: I18n.t("two_factor_authentication.devices.text_confirm_to_complete_html", identifier: @device.identifier) ) end @@ -112,15 +114,15 @@ def validate_device_token # rubocop:disable Metrics/AbcSize def confirm_and_save(result) if result.success? && @device.confirm_registration_and_save - flash[:notice] = t('two_factor_authentication.devices.registration_complete') + flash[:notice] = t("two_factor_authentication.devices.registration_complete") true elsif !result.success? flash[:notice] = nil - flash[:error] = t('two_factor_authentication.devices.registration_failed_token_invalid') + flash[:error] = t("two_factor_authentication.devices.registration_failed_token_invalid") false else flash[:notice] = nil - flash[:error] = t('two_factor_authentication.devices.registration_failed_update') + flash[:error] = t("two_factor_authentication.devices.registration_failed_update") false end end @@ -133,10 +135,10 @@ def request_token_for_device(device, locals) flash[:notice] = transmit.result if transmit.result.present? # Request confirmation from user as in the regular login flow - render 'two_factor_authentication/two_factor_devices/confirm', layout: 'base', locals: + render "two_factor_authentication/two_factor_devices/confirm", layout: "base", locals: else error = transmit.errors.full_messages.join(". ") - default_message = t('two_factor_authentication.devices.confirm_send_failed') + default_message = t("two_factor_authentication.devices.confirm_send_failed") flash[:error] = "#{default_message} #{error}" redirect_to registration_failure_path @@ -195,18 +197,6 @@ def verify_webauthn_credential false end - def webauthn_relying_party - @webauthn_relying_party ||= begin - origin = "#{Setting.protocol}://#{Setting.host_name}" - - WebAuthn::RelyingParty.new( - origin:, - id: URI(origin).host, - name: Setting.app_title - ) - end - end - def logout_other_sessions if current_user == target_user Rails.logger.info { "First 2FA device registered for #{target_user}, terminating other logged in sessions." } @@ -257,7 +247,7 @@ def show_local_breadcrumb end def default_breadcrumb - t('two_factor_authentication.label_devices') + t("two_factor_authentication.label_devices") end def available_devices diff --git a/modules/two_factor_authentication/app/models/two_factor_authentication/device/webauthn.rb b/modules/two_factor_authentication/app/models/two_factor_authentication/device/webauthn.rb index 7379befcd3fc..d36848dbac00 100644 --- a/modules/two_factor_authentication/app/models/two_factor_authentication/device/webauthn.rb +++ b/modules/two_factor_authentication/app/models/two_factor_authentication/device/webauthn.rb @@ -21,15 +21,13 @@ def self.device_type def options_for_create(relying_party) @options_for_create ||= relying_party.options_for_registration( user: { id: user.webauthn_id, name: user.name }, - exclude: TwoFactorAuthentication::Device::Webauthn.where(user:).pluck(:webauthn_external_id), - authenticator_selection: { user_verification: 'discouraged' } + exclude: TwoFactorAuthentication::Device::Webauthn.where(user:).pluck(:webauthn_external_id) ) end def options_for_get(relying_party) @options_for_get ||= relying_party.options_for_authentication( - user_verification: 'discouraged', # we do not require user verification - allow: webauthn_external_id # TODO: Maybe also allow all other tokens? Let's see + allow: [webauthn_external_id] # TODO: Maybe also allow all other tokens? Let's see ) end