diff options
184 files changed, 5073 insertions, 717 deletions
diff --git a/.github/actions/setup-redmine/action.yml b/.github/actions/setup-redmine/action.yml new file mode 100644 index 000000000..d637914f0 --- /dev/null +++ b/.github/actions/setup-redmine/action.yml @@ -0,0 +1,68 @@ +name: Setup Redmine Test Environment +description: Composite action for setting up Redmine test environment + +inputs: + db-type: + description: 'Database type: postgresql, mysql2, or sqlite3. Note: postgresql and mysql2 require service containers to be defined in the workflow.' + required: true + ruby-version: + description: 'Ruby version to use' + required: true + +runs: + using: composite + steps: + - name: Install dependencies and configure environment + shell: bash + run: | + sudo apt-get update + sudo apt-get install --yes --quiet ghostscript gsfonts locales bzr cvs + sudo locale-gen en_US # for bazaar non ascii test + + - name: Allow imagemagick to read PDF files + shell: bash + run: | + echo '<policymap>' > policy.xml + echo '<policy domain="coder" rights="read | write" pattern="PDF" />' >> policy.xml + echo '</policymap>' >> policy.xml + sudo rm /etc/ImageMagick-6/policy.xml + sudo mv policy.xml /etc/ImageMagick-6/policy.xml + + - if: ${{ inputs.db-type == 'sqlite3' }} + name: Prepare test database for sqlite3 + shell: bash + run: | + cat > config/database.yml <<EOF + test: + adapter: sqlite3 + database: db/test.sqlite3 + EOF + + - if: ${{ inputs.db-type == 'mysql2' || inputs.db-type == 'postgresql' }} + name: Prepare test database for mysql2 and postgresql + shell: bash + run: | + cat > config/database.yml <<EOF + test: + adapter: ${{ inputs.db-type }} + database: redmine_test + username: root + password: root + host: 127.0.0.1 + EOF + + - name: Install Ruby and gems + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ inputs.ruby-version }} + bundler-cache: true + + - name: Run prepare test environment + shell: bash + env: + RAILS_ENV: test + SCMS: subversion,git,git_utf8,filesystem,bazaar,cvs + run: | + bundle exec rake ci:about + bundle exec rake ci:setup + bundle exec rake db:environment:set diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c27e3f605..fa8b61666 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,55 +46,11 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Install dependencies and configure environment - run: | - sudo apt-get update - sudo apt-get install --yes --quiet ghostscript gsfonts locales bzr cvs - sudo locale-gen en_US # for bazaar non ascii test - - - name: Allow imagemagick to read PDF files - run: | - echo '<policymap>' > policy.xml - echo '<policy domain="coder" rights="read | write" pattern="PDF" />' >> policy.xml - echo '</policymap>' >> policy.xml - sudo rm /etc/ImageMagick-6/policy.xml - sudo mv policy.xml /etc/ImageMagick-6/policy.xml - - - if: ${{ matrix.db == 'sqlite3' }} - name: Prepare test database for sqlite3 - run: | - cat > config/database.yml <<EOF - test: - adapter: sqlite3 - database: db/test.sqlite3 - EOF - - - if: ${{ matrix.db == 'mysql2' || matrix.db == 'postgresql' }} - name: Prepare test database for mysql2 and postgresql - run: | - cat > config/database.yml <<EOF - test: - adapter: ${{ matrix.db }} - database: redmine_test - username: root - password: root - host: 127.0.0.1 - EOF - - - name: Install Ruby and gems - uses: ruby/setup-ruby@v1 + - name: Setup Redmine test environment + uses: ./.github/actions/setup-redmine with: + db-type: ${{ matrix.db }} ruby-version: ${{ matrix.ruby }} - bundler-cache: true - - - name: Run prepare test environment - env: - RAILS_ENV: test - SCMS: subversion,git,git_utf8,filesystem,bazaar,cvs - run: | - bundle exec rake ci:about - bundle exec rake ci:setup - bundle exec rake db:environment:set - name: Run tests run: | @@ -110,3 +66,38 @@ jobs: - name: Run autoload test run: | bin/rails test:autoload + + system-tests: + name: system test + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Redmine test environment + uses: ./.github/actions/setup-redmine + with: + db-type: sqlite3 + ruby-version: '3.4' + + # System tests use Chrome and ChromeDriver installed on the GitHub Actions Ubuntu image. + # They are generally updated to the latest stable versions. + - name: Display Chrome version + run: google-chrome --version + + - name: Run system tests + run: bin/rails test:system + env: + GOOGLE_CHROME_OPTS_ARGS: headless,disable-gpu,no-sandbox,disable-dev-shm-usage + # System tests might still be a bit unstable, so for now, even if a system test fails, + # output the results and consider the overall test as successful. + continue-on-error: true + + - name: Upload system test screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: system-test-screenshots + path: tmp/screenshots + if-no-files-found: ignore diff --git a/.rubocop.yml b/.rubocop.yml index 3e5f0569f..77857c9cd 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -114,7 +114,10 @@ Naming/VariableNumber: Naming/BinaryOperatorParameterName: Enabled: false -Naming/PredicateName: +Naming/PredicateMethod: + Enabled: false + +Naming/PredicatePrefix: Enabled: false Performance/CollectionLiteralInLoop: @@ -166,6 +169,13 @@ Rails/FindEach: Rails/HelperInstanceVariable: Enabled: false +Rails/Output: + Exclude: + - 'config/routes.rb' + - 'lib/redmine/diff.rb' + - 'lib/redmine/diff_table.rb' + - 'test/unit/lib/redmine/scm/adapters/*.rb' + Rails/Pluck: Exclude: # `pluck` is not available in Gemfile @@ -214,6 +224,9 @@ Style/BlockDelimiters: - 'lib/redmine/string_array_diff/diff.rb' - 'lib/redmine/string_array_diff/diffable.rb' +Style/EmptyStringInsideInterpolation: + Enabled: false + Style/FetchEnvVar: Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3fffc0833..1aafe4925 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --exclude-limit 20 --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.75.2. +# using RuboCop version 1.76.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -220,9 +220,6 @@ Lint/ParenthesesAsGroupedExpression: - 'test/unit/attachment_test.rb' - 'test/unit/lib/redmine/export/pdf_test.rb' -Lint/ShadowingOuterLocalVariable: - Enabled: false - # Configuration parameters: AllowComments, AllowNil. Lint/SuppressedException: Exclude: @@ -331,7 +328,7 @@ Naming/VariableNumber: - 'test/unit/project_test.rb' # Configuration parameters: Severity, Include. -# Include: app/models/**/*.rb +# Include: **/app/models/**/*.rb Rails/ActiveRecordOverride: Exclude: - 'app/models/email_address.rb' @@ -402,7 +399,7 @@ Rails/EagerEvaluationLogMessage: - 'app/controllers/application_controller.rb' # Configuration parameters: Include. -# Include: app/**/*.rb, config/**/*.rb, lib/**/*.rb +# Include: **/app/**/*.rb, **/config/**/*.rb, **/lib/**/*.rb Rails/Exit: Exclude: - 'config/environment.rb' @@ -410,7 +407,7 @@ Rails/Exit: - 'config/routes.rb' # Configuration parameters: Include. -# Include: app/models/**/*.rb +# Include: **/app/models/**/*.rb Rails/HasAndBelongsToMany: Exclude: - 'app/models/changeset.rb' @@ -425,7 +422,7 @@ Rails/HasAndBelongsToMany: - 'app/models/user.rb' # Configuration parameters: Include. -# Include: app/models/**/*.rb +# Include: **/app/models/**/*.rb Rails/HasManyOrHasOneDependent: Exclude: - 'app/models/auth_source.rb' @@ -459,7 +456,7 @@ Rails/I18nLocaleTexts: - 'app/models/mailer.rb' # Configuration parameters: IgnoreScopes, Include. -# Include: app/models/**/*.rb +# Include: **/app/models/**/*.rb Rails/InverseOf: Exclude: - 'app/models/board.rb' @@ -483,7 +480,7 @@ Rails/InverseOf: - 'app/models/wiki_page.rb' # Configuration parameters: Include. -# Include: app/controllers/**/*.rb, app/mailers/**/*.rb +# Include: **/app/controllers/**/*.rb, **/app/mailers/**/*.rb Rails/LexicallyScopedActionFilter: Exclude: - 'app/controllers/projects_controller.rb' @@ -495,7 +492,7 @@ Rails/LinkToBlank: # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Include. -# Include: config/routes.rb, config/routes/**/*.rb +# Include: **/config/routes.rb, **/config/routes/**/*.rb Rails/MatchRoute: Exclude: - 'config/routes.rb' @@ -504,15 +501,6 @@ Rails/MatchRoute: Rails/NegateInclude: Enabled: false -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Include. -# Include: app/**/*.rb, config/**/*.rb, db/**/*.rb, lib/**/*.rb -Rails/Output: - Exclude: - - 'config/routes.rb' - - 'lib/redmine/diff.rb' - - 'lib/redmine/diff_table.rb' - Rails/OutputSafety: Enabled: false @@ -541,7 +529,7 @@ Rails/Present: # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Include. -# Include: app/models/**/*.rb +# Include: **/app/models/**/*.rb Rails/ReadWriteAttribute: Exclude: - 'app/models/auth_source_ldap.rb' @@ -600,7 +588,7 @@ Rails/TimeZone: # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Include. -# Include: app/models/**/*.rb +# Include: **/app/models/**/*.rb Rails/Validation: Enabled: false @@ -708,8 +696,8 @@ Style/CaseLikeIf: # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules. # SupportedStyles: nested, compact -# SupportedStylesForClasses: , nested, compact -# SupportedStylesForModules: , nested, compact +# SupportedStylesForClasses: ~, nested, compact +# SupportedStylesForModules: ~, nested, compact Style/ClassAndModuleChildren: Enabled: false @@ -11,17 +11,21 @@ gem 'marcel' gem 'mail', '~> 2.8.1' gem 'nokogiri', '~> 1.18.3' gem 'i18n', '~> 1.14.1' -gem 'rbpdf', '~> 1.21.3' +gem 'rbpdf', '~> 1.21.4' gem 'addressable' gem 'rubyzip', '~> 2.4.0' gem 'propshaft', '~> 1.1.0' gem 'rack', '>= 3.1.3' gem "stimulus-rails", "~> 1.3" gem "importmap-rails", "~> 2.0" +gem 'commonmarker', '~> 2.3.0' +gem "doorkeeper", "~> 5.8.2" +gem "bcrypt", require: false +gem "doorkeeper-i18n", "~> 5.2" # Ruby Standard Gems gem 'csv', '~> 3.3.2' -gem 'net-imap', '~> 0.5.4' +gem 'net-imap', '~> 0.5.7' gem 'net-pop', '~> 0.1.2' gem 'net-smtp', '~> 0.5.0' @@ -46,12 +50,6 @@ group :minimagick do gem 'mini_magick', '~> 5.2.0' end -# Optional CommonMark support, not for JRuby -group :common_mark do - gem "commonmarker", '~> 2.3.0' - gem 'deckar01-task_list', '2.3.2' -end - # Include database gems for the adapters found in the database # configuration file database_file = File.join(File.dirname(__FILE__), "config/database.yml") @@ -74,6 +72,9 @@ if File.exist?(database_file) when /mysql2/ gem 'mysql2', '~> 0.5.0' gem "with_advisory_lock" + when /trilogy/ + gem 'trilogy', '~> 2.9.0' + gem "with_advisory_lock" when /postgresql/ gem 'pg', '~> 1.5.3' when /sqlite3/ @@ -104,7 +105,7 @@ group :development do end group :test do - gem "rails-dom-testing" + gem "rails-dom-testing", '>= 2.3.0' gem 'mocha', '>= 2.0.1' gem 'simplecov', '~> 0.22.0', :require => false gem "ffi", platforms: [:mingw, :x64_mingw, :mswin] @@ -113,10 +114,13 @@ group :test do gem "capybara", ">= 3.39" gem 'selenium-webdriver', '>= 4.11.0' # RuboCop - gem 'rubocop', '~> 1.75.2', require: false + gem 'rubocop', '~> 1.76.0', require: false gem 'rubocop-performance', '~> 1.25.0', require: false - gem 'rubocop-rails', '~> 2.31.0', require: false + gem 'rubocop-rails', '~> 2.32.0', require: false gem 'bundle-audit', require: false + # for testing oauth provider capabilities + gem 'oauth2' + gem 'rest-client' end local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local") diff --git a/app/assets/images/file-rss.svg b/app/assets/images/file-rss.svg deleted file mode 100644 index 72ab4e29b..000000000 --- a/app/assets/images/file-rss.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="#169"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 2l.117 .007a1 1 0 0 1 .876 .876l.007 .117v4l.005 .15a2 2 0 0 0 1.838 1.844l.157 .006h4l.117 .007a1 1 0 0 1 .876 .876l.007 .117v9a3 3 0 0 1 -2.824 2.995l-.176 .005h-10a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-14a3 3 0 0 1 2.824 -2.995l.176 -.005zm-3 11a1 1 0 0 0 0 2a2 2 0 0 1 1.995 1.85l.005 .15a1 1 0 0 0 2 0a4 4 0 0 0 -4 -4m0 -3a1 1 0 0 0 0 2a5 5 0 0 1 5 5a1 1 0 0 0 2 0a7 7 0 0 0 -7 -7m.01 6h-.01a1 1 0 0 0 -.117 1.993l.127 .007a1 1 0 0 0 0 -2m5.989 -13.001l4.001 4.001h-4z" /></svg>
\ No newline at end of file diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg index 55e3a042d..6283537ce 100644 --- a/app/assets/images/icons.svg +++ b/app/assets/images/icons.svg @@ -11,6 +11,11 @@ <path d="M9 12h6"/> <path d="M12 9v6"/> </symbol> + <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--alert-circle"> + <path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"/> + <path d="M12 8v4"/> + <path d="M12 16h.01"/> + </symbol> <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--angle-down"> <path d="M6 9l6 6l6 -6"/> </symbol> @@ -54,6 +59,13 @@ <path d="M12 15v6"/> <path d="M5 15h3l-3 6h3"/> </symbol> + <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--apps"> + <path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/> + <path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/> + <path d="M14 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/> + <path d="M14 7l6 0"/> + <path d="M17 4l0 6"/> + </symbol> <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--arrow-right"> <path d="M4 9h8v-3.586a1 1 0 0 1 1.707 -.707l6.586 6.586a1 1 0 0 1 0 1.414l-6.586 6.586a1 1 0 0 1 -1.707 -.707v-3.586h-8a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1z"/> </symbol> @@ -72,6 +84,11 @@ <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--bookmarked"> <path d="M18 7v14l-6 -4l-6 4v-14a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4z"/> </symbol> + <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--bulb"> + <path d="M3 12h1m8 -9v1m8 8h1m-15.4 -6.4l.7 .7m12.1 -.7l-.7 .7"/> + <path d="M9 16a5 5 0 1 1 6 0a3.5 3.5 0 0 0 -1 3a2 2 0 0 1 -4 0a3.5 3.5 0 0 0 -1 -3"/> + <path d="M9.7 17l4.6 0"/> + </symbol> <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--bullet-end"> <path d="M12 21a9 9 0 1 0 0 -18a9 9 0 0 0 0 18"/> <path d="M8 12l4 4"/> @@ -294,6 +311,11 @@ <path d="M8 13h6"/> <path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z"/> </symbol> + <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--message-report"> + <path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z"/> + <path d="M12 8v3"/> + <path d="M12 14v.01"/> + </symbol> <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--move"> <path d="M15 14l4 -4l-4 -4"/> <path d="M19 10h-11a4 4 0 1 0 0 8h1"/> @@ -336,6 +358,10 @@ <path d="M7 5.03v5.455"/> <path d="M12 8l5 -3"/> </symbol> + <symbol viewBox="0 0 24 24" id="icon--quote-filled"> + <path d="M9 5a2 2 0 0 1 2 2v6c0 3.13 -1.65 5.193 -4.757 5.97a1 1 0 1 1 -.486 -1.94c2.227 -.557 3.243 -1.827 3.243 -4.03v-1h-3a2 2 0 0 1 -1.995 -1.85l-.005 -.15v-3a2 2 0 0 1 2 -2z"/> + <path d="M18 5a2 2 0 0 1 2 2v6c0 3.13 -1.65 5.193 -4.757 5.97a1 1 0 1 1 -.486 -1.94c2.227 -.557 3.243 -1.827 3.243 -4.03v-1h-3a2 2 0 0 1 -1.995 -1.85l-.005 -.15v-3a2 2 0 0 1 2 -2z"/> + </symbol> <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--reload"> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"/> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"/> @@ -379,6 +405,10 @@ <path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"/> <path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"/> </symbol> + <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--shield-check"> + <path d="M11.46 20.846a12 12 0 0 1 -7.96 -14.846a12 12 0 0 0 8.5 -3a12 12 0 0 0 8.5 3a12 12 0 0 1 -.09 7.06"/> + <path d="M15 19l2 2l4 -4"/> + </symbol> <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--stats"> <path d="M3 13a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/> <path d="M15 9a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/> @@ -459,6 +489,13 @@ <path d="M19 15v6h3"/> <path d="M11 21v-6l2.5 3l2.5 -3v6"/> </symbol> + <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--thumb-up"> + <path d="M7 11v8a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-7a1 1 0 0 1 1 -1h3a4 4 0 0 0 4 -4v-1a2 2 0 0 1 4 0v5h3a2 2 0 0 1 2 2l-1 5a2 3 0 0 1 -2 2h-7a3 3 0 0 1 -3 -3"/> + </symbol> + <symbol viewBox="0 0 24 24" id="icon--thumb-up-filled"> + <path d="M13 3a3 3 0 0 1 2.995 2.824l.005 .176v4h2a3 3 0 0 1 2.98 2.65l.015 .174l.005 .176l-.02 .196l-1.006 5.032c-.381 1.626 -1.502 2.796 -2.81 2.78l-.164 -.008h-8a1 1 0 0 1 -.993 -.883l-.007 -.117l.001 -9.536a1 1 0 0 1 .5 -.865a2.998 2.998 0 0 0 1.492 -2.397l.007 -.202v-1a3 3 0 0 1 3 -3z"/> + <path d="M5 10a1 1 0 0 1 .993 .883l.007 .117v9a1 1 0 0 1 -.883 .993l-.117 .007h-1a2 2 0 0 1 -1.995 -1.85l-.005 -.15v-7a2 2 0 0 1 1.85 -1.995l.15 -.005h1z"/> + </symbol> <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--time"> <path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"/> <path d="M12 7v5l3 3"/> diff --git a/app/assets/javascripts/application-legacy.js b/app/assets/javascripts/application-legacy.js index 265ac39c6..f7c1de95c 100644 --- a/app/assets/javascripts/application-legacy.js +++ b/app/assets/javascripts/application-legacy.js @@ -426,7 +426,7 @@ function showIssueHistory(journal, url) { tab_content.find('.journal').show(); tab_content.find('.journal:not(.has-notes)').hide(); tab_content.find('.journal .wiki').show(); - tab_content.find('.journal .contextual .journal-actions').show(); + tab_content.find('.journal .contextual .journal-actions > *').show(); // always show thumbnails in notes tab var thumbnails = tab_content.find('.journal .thumbnails'); @@ -439,13 +439,15 @@ function showIssueHistory(journal, url) { tab_content.find('.journal:not(.has-details)').hide(); tab_content.find('.journal .wiki').hide(); tab_content.find('.journal .thumbnails').hide(); - tab_content.find('.journal .contextual .journal-actions').hide(); + tab_content.find('.journal .contextual .journal-actions > *').hide(); + // Show reaction button in properties tab + tab_content.find('.journal .contextual .journal-actions .reaction-button-wrapper').show(); break; default: tab_content.find('.journal').show(); tab_content.find('.journal .wiki').show(); tab_content.find('.journal .thumbnails').show(); - tab_content.find('.journal .contextual .journal-actions').show(); + tab_content.find('.journal .contextual .journal-actions > *').show(); } return false; @@ -679,7 +681,7 @@ function copyDataClipboardTextToClipboard(target) { } function setupCopyButtonsToPreElements() { - document.querySelectorAll('pre:not(.pre-wrapper pre)').forEach((pre) => { + document.querySelectorAll('.wiki pre:not(.pre-wrapper pre)').forEach((pre) => { // Wrap the <pre> element with a container and add a copy button const wrapper = document.createElement("div"); wrapper.classList.add("pre-wrapper"); @@ -1222,8 +1224,8 @@ function setupWikiTableSortableHeader() { }); } -function setupHoverTooltips() { - $("[title]:not(.no-tooltip)").tooltip({ +function setupHoverTooltips(container) { + $(container || 'body').find("[title]:not(.no-tooltip)").tooltip({ show: { delay: 400 }, @@ -1233,7 +1235,9 @@ function setupHoverTooltips() { } }); } - +function removeHoverTooltips(container) { + $(container || 'body').find("[title]:not(.no-tooltip)").tooltip('destroy') +} $(function() { setupHoverTooltips(); }); function inlineAutoComplete(element) { diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index d6288ad4f..833e998f8 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -236,6 +236,7 @@ a.user.user-mention { #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;} #sidebar a.selected:hover {text-decoration:none;} +#sidebar a.selected svg.icon-svg { stroke: #fff !important; } #sidebar .query.default {font-weight: bold;} #admin-menu a {line-height:1.7em;} #admin-menu a.selected:not(:has(svg)) {padding-left: 20px !important; background-position: 2px 40%;} @@ -316,7 +317,7 @@ div + .drdn-items {border-top:1px solid #ccc;} width:100%; height:24px; display:inline-block; - padding:3px 18px 3px 6px; + padding:1.5px 18px 3px 6px; border-radius:3px; border:1px solid #ccc; margin:0 !important; @@ -358,11 +359,14 @@ table.list td.buttons a, div.buttons a, table.list td.buttons span.icon-only { m table.list td.buttons a:last-child, div.buttons a:last-child { margin-right: 0; } table.list td.buttons img, div.buttons img {vertical-align:middle;} table.list td.reorder {width:15%; white-space:nowrap; text-align:center; } -table.list table.progress td {padding-right:0px;} +table.list table.progress td {padding-right:0; border-top: none;} table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; } table.list tr.overdue td.due_date { color: #c22; } table.list thead.related-issues th { background-color: inherit; font-size: 11px; border: none; } #role-permissions-trackers table.list th {white-space:normal;} +table.list div.wiki p { + margin: 0; +} .table-list-cell {display: table-cell; vertical-align: top; padding:2px; } .table-list div.buttons {width: 15%;} @@ -679,6 +683,36 @@ div.issue .attribute.string_cf .value .wiki p {margin-top: 0; margin-bottom: 0;} div.issue .attribute.text_cf .value .wiki p:first-of-type {margin-top: 0;} div.issue.overdue .due-date .value { color: #c22; } body.controller-issues h2.inline-flex {padding-right: 0} +div#sticky-issue-header { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + background-color: white; + border-bottom: 1px solid #d0d7de; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + font-size: 0.8125rem; + align-items: center; + z-index: 1000; + padding: 10px 6px; + border-radius: 0px; +} +div#sticky-issue-header.is-visible { + display: flex; +} +div#sticky-issue-header .issue-heading { + flex-shrink: 0; + white-space: nowrap; + margin-right: 6px; +} +div#sticky-issue-header .subject { + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex-grow: 1; +} #issue_tree table.issues, #relations table.issues {border: 0;} #issue_tree table.issues td, #relations table.issues td {border: 0;} @@ -764,6 +798,12 @@ div.journal span.update-info {color: #666; font-size: 0.9em;} #history div:target h4.note-header {background-color:#DDEEFF;} #history p.nodata {display: none;} +/* Prevent content from being hidden behind a #sticky-issue-header when scrolling via anchor links. */ +.controller-issues.action-show div.wiki a[name], +.controller-issues.action-show #history div[id^="note-"], +.controller-issues.action-show #history div[id^="change-"] { + scroll-margin-top: 50px; +} div#activity dl, #search-results { margin-left: 2em; } div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 22px; font-size: 0.8125rem;} @@ -885,7 +925,11 @@ ul.projects div.description ul li {list-style-type:initial;} background-image: none; padding-left: 0; } -#projects-index ul.projects div.root svg { +#projects-index ul.projects .icon-bookmarked-project svg, +#projects-index ul.projects .my-project svg { + margin-left: 4px; +} +#projects-index ul.projects div.root .icon-bookmarked-project svg, #projects-index ul.projects div.root .my-project svg { stroke-width: 2; margin-bottom: 10px; } @@ -896,7 +940,12 @@ ul.projects div.description ul li {list-style-type:initial;} background-image: none; padding-left: 0; } -#projects-index a.project ~ svg, table.projects tr.project td.name svg { +#projects-index div.wiki p { + margin-top: 0px; +} + +table.projects td.name .icon-bookmarked-project svg, +table.projects td.name .my-project svg { margin-left: 4px; } @@ -1060,17 +1109,14 @@ input#months { width: 46px; } .jstBlock .jstTabs { padding-right: 6px; } .jstBlock .wiki-preview { padding: 2px; } -.jstBlock .wiki-preview p:first-child { padding-top: 0 !important; margin-top: 0 !important;} -.jstBlock .wiki-preview p:last-child { padding-bottom: 0 !important; margin-bottom: 0 !important;} +.jstBlock .wiki-preview > p:first-child { padding-top: 0 !important; margin-top: 0 !important;} +.jstBlock .wiki-preview > p:last-child { padding-bottom: 0 !important; margin-bottom: 0 !important;} .tabular .wiki-preview, .tabular .jstTabs {width: 95%;} .tabular.settings .wiki-preview, .tabular.settings .jstTabs { width: 99%; } .tabular.settings .wiki-preview p {padding-left: 0 !important} .tabular .wiki-preview p { min-height: initial; - padding: 0; - padding-top: 1em !important; - padding-bottom: 1em !important; overflow: initial; } @@ -1114,10 +1160,26 @@ span.required {color: #bb0000;} .attachments_fields .icon-attachment, #existing-attachments .icon-attachment {background-image: none; padding-left: 0} .attachments_fields input.filename, #existing-attachments .filename {border:0; width:250px; color:#555; background-color:inherit; } .tabular input.filename {max-width:75% !important;} -.attachments_fields input.filename {height:1.8em;padding-right: 0;} -.attachments_fields .ajax-waiting input.filename {background:url(/hourglass.png) no-repeat 0px 50%;} -.attachments_fields .ajax-loading input.filename {background:url(/loading.gif) no-repeat 0px 50%;} .attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; } +.attachments_fields input.filename { + height:1.8em; + padding-left: 3px; + padding-right: 0; +} +.attachments_fields .ajax-waiting { + padding-left: 16px; + background:url(/hourglass.png) no-repeat 0px 50%; +} +.attachments_fields .ajax-waiting .svg-attachment { + display: none; +} +.attachments_fields .ajax-loading { + padding-left: 16px; + background: url(/loading.gif) no-repeat 0px 50%; +} +.attachments_fields .ajax-loading .svg-attachment { + display: none; +} a.remove-upload:hover {text-decoration:none !important;} .existing-attachment.deleted .filename {text-decoration:line-through; color:#999 !important;} @@ -1136,8 +1198,6 @@ div.thumbnail img {margin: 3px; vertical-align: middle;} p.other-formats { text-align: right; font-size:0.9em; color: #666; } .other-formats span + span:before { content: "| "; } -a.atom { background: url(/file-rss.svg) no-repeat 1px 50%; padding: 2px 0px 3px 16px; } - em.info {font-style:normal;display:block;font-size:90%;color:#888;} em.info.error {padding-left:20px; background:url(/exclamation.png) no-repeat 0 50%;} @@ -1256,11 +1316,45 @@ div.flash.warning svg.icon-svg, .conflict svg.icon-svg { color: #A6750C; } +.warning .oauth-permissions { display:inline-block;text-align:left; } +.warning .oauth-permissions p { margin-top:0;-webkit-margin-before:0;} + #errorExplanation ul { font-size: 0.9em;} #errorExplanation h2, #errorExplanation p { display: none; } .conflict-details {font-size:93%;} +/***** CommonMark Alerts *****/ +.markdown-alert { + border-left: 4px solid; + padding-left: 0.6em; + margin: 1em 0; +} + +.markdown-alert-title { + font-weight: bold; +} + +.markdown-alert-tip { border-color: #5db651; } +.markdown-alert-tip .markdown-alert-title { color: #005f00; } +.markdown-alert-tip .markdown-alert-title svg {stroke: #005f00; } + +.markdown-alert-important { border-color: #800080; } +.markdown-alert-important .markdown-alert-title { color: #4b006e; } +.markdown-alert-important .markdown-alert-title svg { stroke: #4b006e; } + +.markdown-alert-caution { border-color: #c22; } +.markdown-alert-caution .markdown-alert-title { color: #880000; } +.markdown-alert-caution .markdown-alert-title svg { stroke: #880000; } + +.markdown-alert-warning { border-color: #e4bc4b; } +.markdown-alert-warning .markdown-alert-title { color: #a7760c; } +.markdown-alert-warning .markdown-alert-title svg { stroke: #a7760c; } + +.markdown-alert-note { border-color: #169; } +.markdown-alert-note .markdown-alert-title { color: #1e40af; } +.markdown-alert-note .markdown-alert-title svg { stroke: #1e40af; } + /***** Ajax indicator ******/ #ajax-indicator { position: absolute; /* fixed not supported by IE */ @@ -1535,7 +1629,12 @@ div.wiki .external { div.wiki a {word-wrap: break-word;} div.wiki a.new {color: #b73535;} -div.wiki p {line-height: 1.6;} +div.wiki p { + line-height: 1.6; + margin-top: 1em; + margin-bottom: 1em; + padding: 0; +} div.wiki ul, div.wiki ol {margin-bottom:1em;} div.wiki li {line-height: 1.6; margin-bottom: 0.125rem;} div.wiki li>ul, div.wiki li>ol {margin-bottom: 0;} @@ -1570,7 +1669,7 @@ div.pre-wrapper a.copy-pre-content-link { border-radius: 3px; background: #fff; border: 1px solid #ccc; - padding: 2px; + padding: 0px 3px 3px 3px; } div.pre-wrapper:hover a.copy-pre-content-link { @@ -1610,10 +1709,11 @@ a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; } h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor, h4:hover a.wiki-anchor, h5:hover a.wiki-anchor, h6:hover a.wiki-anchor { display: inline; color: #ddd; } div.wiki img {vertical-align:middle; max-width:100%;} -div.wiki>.task-list { - padding-left: 0px; + +div.wiki>.contains-task-list { + padding-left: 0; } -div.wiki .task-list { +div.wiki .contains-task-list { list-style-type: none; } div.wiki .task-list input.task-list-item-checkbox { @@ -1819,10 +1919,15 @@ td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container { flex-shrink: 0; } -a.icon:hover svg, a.icon-only:hover svg { +a.icon:hover .icon-svg, a.icon-only:hover .icon-svg { stroke: #c61a1a; } +a.icon:hover .icon-svg-filled, a.icon-only:hover .icon-svg-filled { + stroke: none; + fill: #c61a1a; +} + svg.icon-ok { stroke: #5db651; } @@ -1846,6 +1951,11 @@ svg.icon-svg { vertical-align: middle; } +svg.icon-svg-filled { + fill: #169; + stroke: none; +} + svg.s20 { width: 1.25rem; height: 1.25rem; @@ -2052,6 +2162,42 @@ color: #555; text-shadow: 1px 1px 0 #fff; img.filecontent.image {background-image: url(/transparent.png);} +/* Reaction styles */ +.reaction-button:hover, .reaction-button:active { + text-decoration: none; +} +.reaction-button .icon-label { + margin-left: 3px; + margin-bottom: -1px; + font-weight: bold; +} +.reaction-button.readonly { + cursor: default; +} +.reaction-button.readonly .icon-svg { + stroke: #999; +} +.reaction-button.readonly .icon-label { + color: #999; +} +div.issue.details .reaction { + float: right; + font-size: 0.9em; + margin-top: 0.5em; + margin-left: 10px; + clear: right; +} +div.message .reaction { + float: right; + font-size: 0.9em; + margin-left: 10px; +} +div.news .reaction { + float: right; + font-size: 0.9em; + margin-left: 10px; +} + /* Custom JQuery styles */ .ui-autocomplete, .ui-menu { border-radius: 2px; diff --git a/app/assets/stylesheets/responsive.css b/app/assets/stylesheets/responsive.css index c5278c87f..b3e8bddd8 100644 --- a/app/assets/stylesheets/responsive.css +++ b/app/assets/stylesheets/responsive.css @@ -385,7 +385,7 @@ list-style: none; } - .flyout-menu #watchers { + .flyout-menu #watchers, .flyout-menu .queries { display: -webkit-flex; display: -webkit-box; display: flex; @@ -402,11 +402,11 @@ order: 3; } - .flyout-menu #watchers h3 { + #sidebar-wrapper { margin-left: -8px; } - .flyout-menu #watchers ul li { + .flyout-menu #watchers ul li, .flyout-menu ul.queries li { display: -webkit-flex; display: -webkit-box; display: flex; @@ -418,6 +418,16 @@ -webkit-align-items: center; -webkit-box-align: center; align-items: center; + border-top: 1px solid rgba(255,255,255,.1); + } + + .flyout-menu #watchers ul li a, .flyout-menu ul.queries li a { + border-top: none; + } + + .flyout-menu ul.queries li a.icon-clear-query { + flex-shrink: 0; + padding-right: 8px; } .flyout-menu ul li a { @@ -440,7 +450,7 @@ color: white; } - .flyout-menu .icon svg { + .flyout-menu .icon svg, .flyout-menu .icon-only svg { stroke: white; } @@ -848,6 +858,19 @@ font-size: 1.1em; text-align: left; } + + /* Sticky issue header */ + /* When project-jump.drdn is visible in mobile layout, offset the sticky header by its height to prevent it from being hidden. */ + div#sticky-issue-header { + top: 64px; + } + + /* Prevent content from being hidden behind #sticky-issue-header and project-jump when scrolling via anchor links. */ + .controller-issues.action-show div.wiki a[name], + .controller-issues.action-show #history div[id^="note-"], + .controller-issues.action-show #history div[id^="change-"] { + scroll-margin-top: 114px; + } } @media all and (max-width: 599px) { diff --git a/app/assets/stylesheets/rtl.css b/app/assets/stylesheets/rtl.css index be609dfab..e7daf7ae8 100644 --- a/app/assets/stylesheets/rtl.css +++ b/app/assets/stylesheets/rtl.css @@ -238,7 +238,7 @@ a.remove-upload {background: url(/delete.png) no-repeat right 1px top 50%; paddi div.thumbnails div {margin-right:0px; margin-left:2px;} -p.other-formats { text-align:left; } +p.other-formats, p.query-totals { text-align:left; } a.atom { background: url(/feed.png) no-repeat right 1px top 50%; padding: 2px 16px 3px 0; } diff --git a/app/assets/stylesheets/wiki_syntax.css b/app/assets/stylesheets/wiki_syntax.css index d326a3293..89b117419 100644 --- a/app/assets/stylesheets/wiki_syntax.css +++ b/app/assets/stylesheets/wiki_syntax.css @@ -1,19 +1,33 @@ @font-face { - font-family: "Noto Sans"; - src: url("/NotoSans-VariableFont_wdth,wght.woff2") format("woff2"); - font-weight: 100 900; - font-stretch: 75% 125%; - font-style: normal; - font-display: swap; + font-family: "Noto Sans"; + src: url("/NotoSans-Regular.woff2") format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; } @font-face { - font-family: "Noto Sans"; - src: url("/NotoSans-Italic-VariableFont_wdth,wght.woff2") format("woff2"); - font-weight: 100 900; - font-stretch: 75% 125%; - font-style: italic; - font-display: swap; + font-family: "Noto Sans"; + src: url("/NotoSans-Bold.woff2") format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Noto Sans"; + src: url("/NotoSans-Italic.woff2") format("woff2"); + font-weight: 400; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: "Noto Sans"; + src: url("/NotoSans-BoldItalic.woff2") format("woff2"); + font-weight: 700; + font-style: italic; + font-display: swap; } :root { @@ -58,3 +72,14 @@ a:hover, a:active{ color: #c61a1a; text-decoration: underline;} .syntaxhl .s1 { background-color: #fff0f0 } span.more_info { font-weight: normal; } + +.markdown-alert { + border-left: 4px solid; + padding-left: 10px; + margin-left: 10px; +} +.markdown-alert-title { + font-weight: bold; +} +.markdown-alert-note { border-color: #169; } +.markdown-alert-note .markdown-alert-title { color: #1e40af; }
\ No newline at end of file diff --git a/app/assets/stylesheets/wiki_syntax_detailed.css b/app/assets/stylesheets/wiki_syntax_detailed.css index 7d7c30f53..ad3c8c65f 100644 --- a/app/assets/stylesheets/wiki_syntax_detailed.css +++ b/app/assets/stylesheets/wiki_syntax_detailed.css @@ -1,19 +1,33 @@ @font-face { - font-family: "Noto Sans"; - src: url("/NotoSans-VariableFont_wdth,wght.woff2") format("woff2"); - font-weight: 100 900; - font-stretch: 75% 125%; - font-style: normal; - font-display: swap; + font-family: "Noto Sans"; + src: url("/NotoSans-Regular.woff2") format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; } @font-face { - font-family: "Noto Sans"; - src: url("/NotoSans-Italic-VariableFont_wdth,wght.woff2") format("woff2"); - font-weight: 100 900; - font-stretch: 75% 125%; - font-style: italic; - font-display: swap; + font-family: "Noto Sans"; + src: url("/NotoSans-Bold.woff2") format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Noto Sans"; + src: url("/NotoSans-Italic.woff2") format("woff2"); + font-weight: 400; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: "Noto Sans"; + src: url("/NotoSans-BoldItalic.woff2") format("woff2"); + font-weight: 700; + font-style: italic; + font-display: swap; } :root { @@ -49,3 +63,23 @@ table.list td { background-color: #f5f5f5; vertical-align: middle; padding: 0.3e .syntaxhl .o { color: #333333 } .syntaxhl .s2 { background-color: #fff0f0 } .syntaxhl .si { background-color: #eeeeee } + + +.markdown-alert { + border-left: 4px solid; + padding-left: 10px; + margin-left: 20px; +} +.markdown-alert-title { + font-weight: bold; +} +.markdown-alert-tip { border-color: #5db651; } +.markdown-alert-tip .markdown-alert-title { color: #005f00; } +.markdown-alert-important { border-color: #800080; } +.markdown-alert-important .markdown-alert-title { color: #4b006e; } +.markdown-alert-caution { border-color: #c22; } +.markdown-alert-caution .markdown-alert-title { color: #880000; } +.markdown-alert-warning { border-color: #e4bc4b; } +.markdown-alert-warning .markdown-alert-title { color: #a7760c; } +.markdown-alert-note { border-color: #169; } +.markdown-alert-note .markdown-alert-title { color: #1e40af; }
\ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 074392709..a01d5c75f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -131,6 +131,14 @@ class ApplicationController < ActionController::Base if (key = api_key_from_request) # Use API key user = User.find_by_api_key(key) + elsif access_token = Doorkeeper.authenticate(request) + # Oauth + if access_token.accessible? + user = User.active.find_by_id(access_token.resource_owner_id) + user.oauth_scope = access_token.scopes.all.map(&:to_sym) + else + doorkeeper_render_error + end elsif /\ABasic /i.match?(request.authorization.to_s) # HTTP Basic, either username/password or API key/random authenticate_with_http_basic do |username, password| diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index 22daf9f90..8b26bee73 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -51,6 +51,8 @@ class MessagesController < ApplicationController offset(@reply_pages.offset). to_a + Message.preload_reaction_details(@replies) + @reply = Message.new(:subject => "RE: #{@message.subject}") render :action => "show", :layout => false if request.xhr? end diff --git a/app/controllers/news_controller.rb b/app/controllers/news_controller.rb index 06240e359..dd6bade24 100644 --- a/app/controllers/news_controller.rb +++ b/app/controllers/news_controller.rb @@ -67,8 +67,10 @@ class NewsController < ApplicationController end def show - @comments = @news.comments.to_a + @comments = @news.comments.preload(:commented).to_a @comments.reverse! if User.current.wants_comments_in_reverse_order? + + Comment.preload_reaction_details(@comments) end def new diff --git a/app/controllers/oauth2_applications_controller.rb b/app/controllers/oauth2_applications_controller.rb new file mode 100644 index 000000000..107af2ec0 --- /dev/null +++ b/app/controllers/oauth2_applications_controller.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +class Oauth2ApplicationsController < Doorkeeper::ApplicationsController + private + + def application_params + params[:doorkeeper_application] ||= {} + params[:doorkeeper_application][:scopes] ||= [] + + scopes = Redmine::AccessControl.public_permissions.map{|p| p.name.to_s} + + if params[:doorkeeper_application][:scopes].is_a?(Array) + scopes |= params[:doorkeeper_application][:scopes] + else + scopes |= params[:doorkeeper_application][:scopes].split(/\s+/) + end + params[:doorkeeper_application][:scopes] = scopes.join(' ') + super + end +end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index f9a390c58..2a42c99ed 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -176,7 +176,7 @@ class ProjectsController < ApplicationController respond_to do |format| format.html do @principals_by_role = @project.principals_by_role - @subprojects = @project.children.visible.to_a + @subprojects = @project.leaf? ? [] : @project.children.visible.to_a @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").to_a with_subprojects = Setting.display_subprojects_issues? @trackers = @project.rolled_up_trackers(with_subprojects).visible diff --git a/app/controllers/reactions_controller.rb b/app/controllers/reactions_controller.rb new file mode 100644 index 000000000..71b37e5f8 --- /dev/null +++ b/app/controllers/reactions_controller.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class ReactionsController < ApplicationController + before_action :require_login + + before_action :check_enabled + before_action :set_object, :authorize_reactable + + def create + respond_to do |format| + format.js do + @object.reactions.find_or_create_by!(user: User.current) + end + format.any { head :not_found } + end + end + + def destroy + respond_to do |format| + format.js do + reaction = @object.reactions.by(User.current).find_by(id: params[:id]) + reaction&.destroy + end + format.any { head :not_found } + end + end + + private + + def check_enabled + render_403 unless Setting.reactions_enabled? + end + + def set_object + object_type = params[:object_type] + + unless Redmine::Reaction::REACTABLE_TYPES.include?(object_type) + render_403 + return + end + + @object = object_type.constantize.find(params[:object_id]) + end + + def authorize_reactable + render_403 unless Redmine::Reaction.editable?(@object, User.current) + end +end diff --git a/app/controllers/versions_controller.rb b/app/controllers/versions_controller.rb index d52b43ba3..328d3e56e 100644 --- a/app/controllers/versions_controller.rb +++ b/app/controllers/versions_controller.rb @@ -51,7 +51,7 @@ class VersionsController < ApplicationController if @selected_tracker_ids.any? && @versions.any? issues = Issue.visible. includes(:project, :tracker). - preload(:status, :priority, :fixed_version). + preload(:status, :priority, :fixed_version, {:assigned_to => :email_address}). where(:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)). order("#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id") @issues_by_version = issues.group_by(&:fixed_version) @@ -69,7 +69,7 @@ class VersionsController < ApplicationController format.html do @issues = @version.fixed_issues.visible. includes(:status, :tracker, :priority). - preload(:project). + preload(:project, {:assigned_to => :email_address}). reorder("#{Tracker.table_name}.position, #{Issue.table_name}.id"). to_a end diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index 36b90da77..bcb3b0891 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -240,6 +240,7 @@ class WikiController < ApplicationController # don't load text @versions = @page.content.versions. select("id, author_id, comments, updated_on, version"). + preload(:author). reorder('version DESC'). limit(@version_pages.per_page + 1). offset(@version_pages.offset). diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 847fb9fdd..285528422 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -518,6 +518,8 @@ module ApplicationHelper def render_flash_messages s = +'' flash.each do |k, v| + next unless v.is_a?(String) + s << content_tag('div', notice_icon(k) + v.html_safe, :class => "flash #{k}", :id => "flash_#{k}") end s.html_safe @@ -1386,7 +1388,7 @@ module ApplicationHelper <| $) }x - HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE) + HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/im unless const_defined?(:HEADING_RE) def parse_sections(text, project, obj, attr, only_path, options) return unless options[:edit_section_links] diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index b39427bda..88a571b62 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -44,6 +44,7 @@ module AvatarsHelper if user.respond_to?(:mail) email = user.mail options[:title] = user.name unless options[:title] + options[:initials] = user.initials if options[:default] == "initials" elsif user.to_s =~ %r{<(.+?)>} email = $1 end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index f96315c75..6afb84537 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -21,10 +21,10 @@ module IconsHelper DEFAULT_ICON_SIZE = "18" DEFAULT_SPRITE = "icons" - def sprite_icon(icon_name, label = nil, icon_only: false, size: DEFAULT_ICON_SIZE, css_class: nil, sprite: DEFAULT_SPRITE, plugin: nil, rtl: false) + def sprite_icon(icon_name, label = nil, icon_only: false, size: DEFAULT_ICON_SIZE, style: :outline, css_class: nil, sprite: DEFAULT_SPRITE, plugin: nil, rtl: false) sprite = plugin ? "plugin_assets/#{plugin}/#{sprite}.svg" : "#{sprite}.svg" - svg_icon = svg_sprite_icon(icon_name, size: size, css_class: css_class, sprite: sprite, rtl: rtl) + svg_icon = svg_sprite_icon(icon_name, size: size, style: style, css_class: css_class, sprite: sprite, rtl: rtl) if label label_classes = ["icon-label"] @@ -92,8 +92,9 @@ module IconsHelper private - def svg_sprite_icon(icon_name, size: DEFAULT_ICON_SIZE, sprite: DEFAULT_SPRITE, css_class: nil, rtl: false) + def svg_sprite_icon(icon_name, size: DEFAULT_ICON_SIZE, style: :outline, sprite: DEFAULT_SPRITE, css_class: nil, rtl: false) css_classes = "s#{size} icon-svg" + css_classes += " icon-svg-filled" if style == :filled css_classes += " #{css_class}" unless css_class.nil? css_classes += " icon-rtl" if rtl diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 6586a1b7e..ce3607a5d 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -22,6 +22,7 @@ module IssuesHelper include Redmine::Export::PDF::IssuesPdfHelper include IssueStatusesHelper include QueriesHelper + include ReactionsHelper def issue_list(issues, &) ancestors = [] diff --git a/app/helpers/journals_helper.rb b/app/helpers/journals_helper.rb index 6c22fc4ca..7f1a449fb 100644 --- a/app/helpers/journals_helper.rb +++ b/app/helpers/journals_helper.rb @@ -19,6 +19,7 @@ module JournalsHelper include Redmine::QuoteReply::Helper + include ReactionsHelper # Returns the attachments of a journal that are displayed as thumbnails def journal_thumbnail_attachments(journal) @@ -40,6 +41,8 @@ module JournalsHelper ) end + links << reaction_button(journal) + if journal.notes.present? if options[:reply_links] url = quoted_issue_path(issue, :journal_id => journal, :journal_indice => indice) diff --git a/app/helpers/messages_helper.rb b/app/helpers/messages_helper.rb index fd9ba3bcb..92f788d0c 100644 --- a/app/helpers/messages_helper.rb +++ b/app/helpers/messages_helper.rb @@ -19,4 +19,5 @@ module MessagesHelper include Redmine::QuoteReply::Helper + include ReactionsHelper end diff --git a/app/helpers/news_helper.rb b/app/helpers/news_helper.rb index a5c50fdfd..cd7b6734a 100644 --- a/app/helpers/news_helper.rb +++ b/app/helpers/news_helper.rb @@ -18,4 +18,5 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module NewsHelper + include ReactionsHelper end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 01a5452f7..bae1c4e3a 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -80,8 +80,8 @@ module ProjectsHelper classes += %w(icon icon-bookmarked-project) if bookmarked_project_ids.include?(project.id) s = link_to_project(project, {}, :class => classes.uniq.join(' ')) - s << sprite_icon('user', l(:label_my_projects), icon_only: true) if User.current.member_of?(project) - s << sprite_icon('bookmarked', l(:label_my_bookmarks), icon_only: true) if bookmarked_project_ids.include?(project.id) + s << tag.span(sprite_icon('user', l(:label_my_projects), icon_only: true), class: 'icon-only icon-user my-project') if User.current.member_of?(project) + s << tag.span(sprite_icon('bookmarked', l(:label_my_bookmarks), icon_only: true), class: 'icon-only icon-bookmarked-project') if bookmarked_project_ids.include?(project.id) if project.description.present? s << content_tag('div', textilizable(project.short_description, :project => project), :class => 'wiki description') end diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index ca7168f27..3aef7083a 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -169,7 +169,7 @@ module QueriesHelper group_name = format_object(group) end group_name ||= "" - group_count = result_count_by_group ? result_count_by_group[group] : nil + group_count = result_count_by_group&.[](group) group_totals = totals_by_group.map {|column, t| total_tag(column, t[group] || 0)}.join(" ").html_safe end end diff --git a/app/helpers/reactions_helper.rb b/app/helpers/reactions_helper.rb new file mode 100644 index 000000000..e02e1c9f9 --- /dev/null +++ b/app/helpers/reactions_helper.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module ReactionsHelper + # Maximum number of users to display in the reaction button tooltip + DISPLAY_REACTION_USERS_LIMIT = 10 + + def reaction_button(object) + return unless Redmine::Reaction.visible?(object, User.current) + + detail = object.reaction_detail + + user_reaction = detail.user_reaction + count = detail.reaction_count + visible_user_names = detail.visible_users.take(DISPLAY_REACTION_USERS_LIMIT).map(&:name) + + tooltip = build_reaction_tooltip(visible_user_names, count) + + if Redmine::Reaction.editable?(object, User.current) + if user_reaction.present? + reaction_button_reacted(object, user_reaction, count, tooltip) + else + reaction_button_not_reacted(object, count, tooltip) + end + else + reaction_button_readonly(object, count, tooltip) + end + end + + def reaction_id_for(object) + dom_id(object, :reaction) + end + + private + + def reaction_button_reacted(object, reaction, count, tooltip) + reaction_button_wrapper object do + link_to( + sprite_icon('thumb-up-filled', count.nonzero?, style: :filled), + reaction_path(reaction, object_type: object.class.name, object_id: object), + remote: true, method: :delete, + class: ['icon', 'reaction-button', 'reacted'], + title: tooltip + ) + end + end + + def reaction_button_not_reacted(object, count, tooltip) + reaction_button_wrapper object do + link_to( + sprite_icon('thumb-up', count.nonzero?), + reactions_path(object_type: object.class.name, object_id: object), + remote: true, method: :post, + class: 'icon reaction-button', + title: tooltip + ) + end + end + + def reaction_button_readonly(object, count, tooltip) + reaction_button_wrapper object do + tag.span(class: 'icon reaction-button readonly', title: tooltip) do + sprite_icon('thumb-up', count.nonzero?) + end + end + end + + def reaction_button_wrapper(object, &) + tag.span(class: 'reaction-button-wrapper', data: { 'reaction-button-id': reaction_id_for(object) }, &) + end + + def build_reaction_tooltip(visible_user_names, count) + return if count.zero? + + display_user_names = visible_user_names.dup + others = count - visible_user_names.size + + if others.positive? + display_user_names << I18n.t(:reaction_text_x_other_users, count: others) + end + + display_user_names.to_sentence(locale: I18n.locale) + end +end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 39a836a03..c1f989805 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -244,6 +244,7 @@ module SettingsHelper ['Mystery man', 'mm'], ['Retro', 'retro'], ['Robohash', 'robohash'], - ['Wavatars', 'wavatar']] + ['Wavatars', 'wavatar'], + ['Initials', 'initials']] end end diff --git a/app/helpers/watchers_helper.rb b/app/helpers/watchers_helper.rb index 6e6366846..bfed8adf2 100644 --- a/app/helpers/watchers_helper.rb +++ b/app/helpers/watchers_helper.rb @@ -48,7 +48,9 @@ module WatchersHelper def watchers_list(object) remove_allowed = User.current.allowed_to?(:"delete_#{object.class.name.underscore}_watchers", object.project) content = ''.html_safe - lis = object.watcher_users.sorted.collect do |user| + scope = object.watcher_users + scope = scope.includes(:email_address) if Setting.gravatar_enabled? + lis = scope.sorted.collect do |user| s = ''.html_safe s << avatar(user, :size => "16").to_s if user.is_a?(User) s << link_to_principal(user, class: user.class.to_s.downcase) diff --git a/app/javascript/controllers/sticky_issue_header_controller.js b/app/javascript/controllers/sticky_issue_header_controller.js new file mode 100644 index 000000000..aebc7d2dc --- /dev/null +++ b/app/javascript/controllers/sticky_issue_header_controller.js @@ -0,0 +1,22 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = ["original", "stickyHeader"]; + + connect() { + if (!this.originalTarget || !this.stickyHeaderTarget) return; + + this.observer = new IntersectionObserver( + ([entry]) => { + this.stickyHeaderTarget.classList.toggle("is-visible", !entry.isIntersecting); + }, + { threshold: 0 } + ); + + this.observer.observe(this.originalTarget); + } + + disconnect() { + this.observer?.disconnect(); + } +} diff --git a/app/models/comment.rb b/app/models/comment.rb index 79eb59748..1716537af 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -19,6 +19,8 @@ class Comment < ApplicationRecord include Redmine::SafeAttributes + include Redmine::Reaction::Reactable + belongs_to :commented, :polymorphic => true, :counter_cache => true belongs_to :author, :class_name => 'User' @@ -28,6 +30,8 @@ class Comment < ApplicationRecord safe_attributes 'comments' + delegate :visible?, to: :commented + def comments=(arg) self.content = arg end @@ -36,6 +40,10 @@ class Comment < ApplicationRecord content end + def project + commented.respond_to?(:project) ? commented.project : nil + end + private def send_notification diff --git a/app/models/issue.rb b/app/models/issue.rb index ac3b40bf1..576840843 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -25,6 +25,7 @@ class Issue < ApplicationRecord before_validation :clear_disabled_fields before_save :set_parent_id include Redmine::NestedSet::IssueNestedSet + include Redmine::Reaction::Reactable belongs_to :project belongs_to :tracker @@ -916,7 +917,8 @@ class Issue < ApplicationRecord result = journals. preload(:details). preload(:user => :email_address). - reorder(:created_on, :id).to_a + reorder(:created_on, :id). + to_a result.each_with_index {|j, i| j.indice = i + 1} @@ -927,6 +929,9 @@ class Issue < ApplicationRecord end Journal.preload_journals_details_custom_fields(result) result.select! {|journal| journal.notes? || journal.visible_details.any?} + + Journal.preload_reaction_details(result) + result end @@ -1170,7 +1175,7 @@ class Issue < ApplicationRecord if leaf? spent_hours else - self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0 + self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f end end diff --git a/app/models/journal.rb b/app/models/journal.rb index 039b182e2..12f2beec8 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -19,6 +19,7 @@ class Journal < ApplicationRecord include Redmine::SafeAttributes + include Redmine::Reaction::Reactable belongs_to :journalized, :polymorphic => true # added as a quick fix to allow eager loading of the polymorphic association diff --git a/app/models/message.rb b/app/models/message.rb index c7f78d2d9..9ac88c7d1 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -19,6 +19,8 @@ class Message < ApplicationRecord include Redmine::SafeAttributes + include Redmine::Reaction::Reactable + belongs_to :board belongs_to :author, :class_name => 'User' acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC" diff --git a/app/models/news.rb b/app/models/news.rb index 40cd63db9..174e4c5ac 100644 --- a/app/models/news.rb +++ b/app/models/news.rb @@ -19,6 +19,8 @@ class News < ApplicationRecord include Redmine::SafeAttributes + include Redmine::Reaction::Reactable + belongs_to :project belongs_to :author, :class_name => 'User' has_many :comments, lambda {order("created_on")}, :as => :commented, :dependent => :delete_all diff --git a/app/models/principal.rb b/app/models/principal.rb index 77f599c73..86c4edb84 100644 --- a/app/models/principal.rb +++ b/app/models/principal.rb @@ -35,6 +35,8 @@ class Principal < ApplicationRecord :foreign_key => 'user_id' has_many :projects, :through => :memberships has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify + # Always returns nil for groups + has_one :email_address, lambda {where :is_default => true}, :autosave => true, :foreign_key => 'user_id' validate :validate_status diff --git a/app/models/reaction.rb b/app/models/reaction.rb new file mode 100644 index 000000000..184ed2d6e --- /dev/null +++ b/app/models/reaction.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Reaction < ApplicationRecord + belongs_to :reactable, polymorphic: true + belongs_to :user + + validates :reactable_type, inclusion: { in: Redmine::Reaction::REACTABLE_TYPES } + + scope :by, ->(user) { where(user: user) } + scope :for_reactable, ->(reactable) { where(reactable: reactable) } + scope :visible, ->(user) { where(user: User.visible(user)) } + + # Represents reaction details for a reactable object + Detail = Struct.new( + # Users who reacted and are visible to the target user + :visible_users, + # Reaction of the target user + :user_reaction + ) do + def initialize(visible_users: [], user_reaction: nil) + super + end + + def reaction_count = visible_users.size + end + + def self.build_detail_map_for(reactables, user) + reactions = visible(user) + .for_reactable(reactables) + .preload(:user) + .select(:id, :reactable_id, :user_id) + .order(id: :desc) + + reactions.each_with_object({}) do |reaction, m| + m[reaction.reactable_id] ||= Detail.new + + m[reaction.reactable_id].then do |detail| + detail.visible_users << reaction.user + detail.user_reaction = reaction if reaction.user == user + end + end + end +end diff --git a/app/models/role.rb b/app/models/role.rb index 3ca4f92a1..870bbe945 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -198,11 +198,14 @@ class Role < ApplicationRecord # action can be: # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') # * a permission Symbol (eg. :edit_project) - def allowed_to?(action) + # scope can be: + # * an array of permissions which will be used as filter (logical AND) + + def allowed_to?(action, scope=nil) if action.is_a? Hash - allowed_actions.include? "#{action[:controller]}/#{action[:action]}" + allowed_actions(scope).include? "#{action[:controller]}/#{action[:action]}" else - allowed_permissions.include? action + allowed_permissions(scope).include? action end end @@ -298,13 +301,20 @@ class Role < ApplicationRecord private - def allowed_permissions - @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name} + def allowed_permissions(scope = nil) + scope = scope.sort if scope.present? # to maintain stable cache keys + @allowed_permissions ||= {} + @allowed_permissions[scope] ||= begin + unscoped = permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name} + scope.present? ? unscoped & scope : unscoped + end end - def allowed_actions - @actions_allowed ||= - allowed_permissions.inject([]) do |actions, permission| + def allowed_actions(scope = nil) + scope = scope.sort if scope.present? # to maintain stable cache keys + @actions_allowed ||= {} + @actions_allowed[scope] ||= + allowed_permissions(scope).inject([]) do |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) end.flatten end diff --git a/app/models/user.rb b/app/models/user.rb index 1839613c7..496084ceb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -28,46 +28,55 @@ class User < Principal USER_FORMATS = { :firstname_lastname => { :string => '#{firstname} #{lastname}', + :initials => '#{firstname.to_s.first}#{lastname.to_s.first}', :order => %w(firstname lastname id), :setting_order => 1 }, :firstname_lastinitial => { :string => '#{firstname} #{lastname.to_s.chars.first}.', + :initials => '#{firstname.to_s.first}#{lastname.to_s.first}', :order => %w(firstname lastname id), :setting_order => 2 }, :firstinitial_lastname => { :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}', + :initials => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\').first}#{lastname.to_s.first}', :order => %w(firstname lastname id), :setting_order => 2 }, :firstname => { :string => '#{firstname}', + :initials => '#{firstname.to_s.first(2)}', :order => %w(firstname id), :setting_order => 3 }, :lastname_firstname => { :string => '#{lastname} #{firstname}', + :initials => '#{lastname.to_s.first}#{firstname.to_s.first}', :order => %w(lastname firstname id), :setting_order => 4 }, :lastnamefirstname => { :string => '#{lastname}#{firstname}', + :initials => '#{lastname.to_s.first}#{firstname.to_s.first}', :order => %w(lastname firstname id), :setting_order => 5 }, :lastname_comma_firstname => { :string => '#{lastname}, #{firstname}', + :initials => '#{lastname.to_s.first}#{firstname.to_s.first}', :order => %w(lastname firstname id), :setting_order => 6 }, :lastname => { :string => '#{lastname}', + :initials => '#{lastname.to_s.first(2)}', :order => %w(lastname id), :setting_order => 7 }, :username => { :string => '#{login}', + :initials => '#{login.to_s.first(2)}', :order => %w(login id), :setting_order => 8 }, @@ -89,10 +98,10 @@ class User < Principal :after_remove => Proc.new {|user, group| group.user_removed(user)} has_many :changesets, :dependent => :nullify has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' - has_one :atom_token, lambda {where "action='feeds'"}, :class_name => 'Token' - has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token' - has_one :email_address, lambda {where :is_default => true}, :autosave => true + has_one :atom_token, lambda {where "#{table.name}.action='feeds'"}, :class_name => 'Token' + has_one :api_token, lambda {where "#{table.name}.action='api'"}, :class_name => 'Token' has_many :email_addresses, :dependent => :delete_all + has_many :reactions, dependent: :delete_all belongs_to :auth_source scope :logged, lambda {where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}")} @@ -103,6 +112,7 @@ class User < Principal attr_accessor :password, :password_confirmation, :generate_password attr_accessor :last_before_login_on attr_accessor :remote_ip + attr_writer :oauth_scope LOGIN_LENGTH_LIMIT = 60 MAIL_LENGTH_LIMIT = 254 @@ -275,6 +285,14 @@ class User < Principal end end + # Return user's initials based on name format + def initials(formatter = nil) + f = self.class.name_formatter(formatter) + format = f[:initials] || USER_FORMATS[:firstname_lastname][:initials] + initials = eval('"' + format + '"') + initials.upcase + end + def registered? self.status == STATUS_REGISTERED end @@ -715,6 +733,20 @@ class User < Principal end end + def admin? + if authorized_by_oauth? + # when signed in via oauth, the user only acts as admin when the admin scope is set + super and @oauth_scope.include?(:admin) + else + super + end + end + + # true if the user has signed in via oauth + def authorized_by_oauth? + !@oauth_scope.nil? + end + # Return true if the user is allowed to do the specified action on a specific context # Action can be: # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') @@ -735,7 +767,7 @@ class User < Principal roles.any? do |role| (context.is_public? || role.member?) && - role.allowed_to?(action) && + role.allowed_to?(action, @oauth_scope) && (block ? yield(role, self) : true) end elsif context && context.is_a?(Array) @@ -754,7 +786,7 @@ class User < Principal # authorize if user has at least one role that has this permission roles = self.roles.to_a | [builtin_role] roles.any? do |role| - role.allowed_to?(action) && + role.allowed_to?(action, @oauth_scope) && (block ? yield(role, self) : true) end else diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 8b19d9a5a..e1842b131 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -73,7 +73,7 @@ class UserPreference < ApplicationRecord if has_attribute? attr_name super else - others ? others[attr_name] : nil + others&.[](attr_name) end end diff --git a/app/models/version.rb b/app/models/version.rb index 707ed59dc..3ca4f2bff 100644 --- a/app/models/version.rb +++ b/app/models/version.rb @@ -106,7 +106,7 @@ module FixedIssuesExtension done = self.open(open).sum do |c| estimated = c.total_estimated_hours.to_f estimated = estimated_average unless estimated > 0.0 - ratio = c.closed? ? 100 : (c.done_ratio || 0) + ratio = open ? (c.done_ratio || 0) : 100 estimated * ratio end progress = done / (estimated_average * issues_count) diff --git a/app/views/attachments/_form.html.erb b/app/views/attachments/_form.html.erb index c8bb84123..e5b10fb55 100644 --- a/app/views/attachments/_form.html.erb +++ b/app/views/attachments/_form.html.erb @@ -15,6 +15,7 @@ <% if saved_attachments.present? %> <% saved_attachments.each_with_index do |attachment, i| %> <span id="attachments_p<%= i %>"> + <%= sprite_icon('attachment', icon_only: true, size: 16, css_class: 'svg-attachment') %> <%= text_field_tag("#{attachment_param}[p#{i}][filename]", attachment.filename, :class => 'filename') %> <% if attachment.container_id.present? %> <%= link_to sprite_icon('del', l(:button_delete), icon_only: true), "#", :onclick => "$(this).closest('.attachments_form').find('.add_attachment').show(); $(this).parent().remove(); return false;", :class => 'icon-only icon-del' %> diff --git a/app/views/doorkeeper/applications/_form.html.erb b/app/views/doorkeeper/applications/_form.html.erb new file mode 100644 index 000000000..e4f778f63 --- /dev/null +++ b/app/views/doorkeeper/applications/_form.html.erb @@ -0,0 +1,39 @@ +<%= error_messages_for 'application' %> +<div class="box tabular"> + <p><%= f.text_field :name, :required => true %></p> + + <p> + <%= f.text_area :redirect_uri, :required => true, :size => 60, :label => :'activerecord.attributes.doorkeeper/application.redirect_uri' %> + <em class="info"> + <%= t('doorkeeper.applications.help.redirect_uri') %> + </em> + </p> +</div> + +<h3><%= l(:'activerecord.attributes.doorkeeper/application.scopes') %></h3> +<p><em class="info"><%= l :text_oauth_info_scopes %></em></p> +<div class="box tabular" id="scopes"> +<fieldset><legend><%= l :label_oauth_admin_access %></legend> + <label class="floating" style="width: auto;"> + <%= check_box_tag 'doorkeeper_application[scopes][]', 'admin', @application.scopes.include?('admin'), + :id => "doorkeeper_application_scopes_admin" + %> + <%= l :text_oauth_admin_permission %> + </label> +</fieldset> +<% perms_by_module = Redmine::AccessControl.permissions.group_by {|p| p.project_module.to_s} %> +<% perms_by_module.keys.sort.each do |mod| %> + <fieldset><legend><%= mod.blank? ? l(:label_project) : l_or_humanize(mod, :prefix => 'project_module_') %></legend> + <% perms_by_module[mod].each do |permission| %> + <label class="floating"> + <%= check_box_tag 'doorkeeper_application[scopes][]', permission.name.to_s, (permission.public? || @application.scopes.include?( permission.name.to_s)), + :id => "doorkeeper_application_scopes_#{permission.name}", + :disabled => permission.public? %> + <%= l_or_humanize(permission.name, :prefix => 'permission_') %> + </label> + <% end %> + </fieldset> +<% end %> +<br /><%= check_all_links 'scopes' %> +<%= hidden_field_tag 'doorkeeper_application[scopes][]', '' %> +</div> diff --git a/app/views/doorkeeper/applications/edit.html.erb b/app/views/doorkeeper/applications/edit.html.erb new file mode 100644 index 000000000..aebc1a841 --- /dev/null +++ b/app/views/doorkeeper/applications/edit.html.erb @@ -0,0 +1,6 @@ +<%= title [l('label_oauth_application_plural'), oauth_applications_path], @application.name %> + +<%= labelled_form_for @application, url: doorkeeper_submit_path(@application) do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/doorkeeper/applications/index.html.erb b/app/views/doorkeeper/applications/index.html.erb new file mode 100644 index 000000000..0ba31c0e8 --- /dev/null +++ b/app/views/doorkeeper/applications/index.html.erb @@ -0,0 +1,33 @@ +<div class="contextual"> +<%= link_to sprite_icon('add', t('.new')), new_oauth_application_path, :class => 'icon icon-add' %> +</div> + +<%= title l 'label_oauth_application_plural' %> + +<% if @applications.any? %> +<div class="autoscroll"> +<table class="list"> + <thead><tr> + <th><%= t('.name') %></th> + <th><%= t('.callback_url') %></th> + <th><%= t('.scopes') %></th> + <th></th> + </tr></thead> + <tbody> + <% @applications.each do |application| %> + <tr id="application_<%= application.id %>" class="<%= cycle("odd", "even") %>"> + <td class="name"><span><%= link_to application.name, oauth_application_path(application) %></span></td> + <td class="description"><%= truncate application.redirect_uri.split.join(', '), length: 50 %></td> + <td class="description"><%= safe_join application.scopes.map{|scope| h l_or_humanize(scope, prefix: 'permission_')}, ", " %></td> + <td class="buttons"> + <%= link_to sprite_icon('edit', t('doorkeeper.applications.buttons.edit')), edit_oauth_application_path(application), class: 'icon icon-edit' %> + <%= link_to sprite_icon('del', t('doorkeeper.applications.buttons.destroy')), oauth_application_path(application), :data => {:confirm => t('doorkeeper.applications.confirmations.destroy')}, :method => :delete, :class => 'icon icon-del' %> + </td> + </tr> + <% end %> + </tbody> +</table> +</div> +<% else %> + <p class="nodata"><%= l(:label_no_data) %></p> +<% end %> diff --git a/app/views/doorkeeper/applications/new.html.erb b/app/views/doorkeeper/applications/new.html.erb new file mode 100644 index 000000000..e2a39ac93 --- /dev/null +++ b/app/views/doorkeeper/applications/new.html.erb @@ -0,0 +1,6 @@ +<%= title [l('label_oauth_application_plural'), oauth_applications_path], t('.title') %> + +<%= labelled_form_for @application, url: doorkeeper_submit_path(@application) do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<% end %> diff --git a/app/views/doorkeeper/applications/show.html.erb b/app/views/doorkeeper/applications/show.html.erb new file mode 100644 index 000000000..c98e7d29c --- /dev/null +++ b/app/views/doorkeeper/applications/show.html.erb @@ -0,0 +1,54 @@ +<div class="contextual"> +<%= link_to sprite_icon('edit', t('doorkeeper.applications.buttons.edit')), edit_oauth_application_path(@application), :accesskey => accesskey(:edit), class: 'icon icon-edit' %> +<%= link_to sprite_icon('del', t('doorkeeper.applications.buttons.destroy')), oauth_application_path(@application), :data => {:confirm => t('doorkeeper.applications.confirmations.destroy')}, :method => :delete, :class => 'icon icon-del' %> +</div> + +<%= title [l('label_oauth_application_plural'), oauth_applications_path], @application.name %> + +<div class="box"> + <h3 class="icon icon-passwd"><%= sprite_icon('key', l(:label_information_plural)) %></h3> + <p> + <span class="label"><%= t('.application_id') %>:</span> + <code><%= h @application.uid %></code> + </p> + <p> + <span class="label"><%= t('.secret') %>:</span> + <code> + <% secret = flash[:application_secret].presence || @application.plaintext_secret %> + <% flash.delete :application_secret %> + <% if secret.blank? && Doorkeeper.config.application_secret_hashed? %> + <%= t('.secret_hashed') %> + <% else %> + <%= secret %> + <% end %> + </code> + <% if secret.present? && Doorkeeper.config.application_secret_hashed? %> + <strong><%= t "text_oauth_copy_secret_now" %></strong> + <% end %> + </p> + <p> + <span class="label"><%= t('.scopes') %>:</span> + <code><%= safe_join @application.scopes.map{|scope| h l_or_humanize(scope, prefix: 'permission_')}, ", " %></code> + </p> +</div> + +<h3><%= t('.callback_urls') %></h3> + +<div class="autoscroll"> +<table class="list"> + <thead><tr> + <th><%= t('.callback_url') %></th> + <th></th> + </tr></thead> + <tbody> + <% @application.redirect_uri.split.each do |uri| %> + <tr class="<%= cycle("odd", "even") %>"> + <td class="name"><span><%= uri %></span></td> + <td class="buttons"> + <%= link_to sprite_icon('shield-check', t('doorkeeper.applications.buttons.authorize')), oauth_authorization_path(client_id: @application.uid, redirect_uri: uri, response_type: 'code', scope: @application.scopes), class: 'icon icon-authorize', target: '_blank' %> + </td> + </tr> + <% end %> + </tbody> +</table> +</div> diff --git a/app/views/doorkeeper/authorizations/error.html.erb b/app/views/doorkeeper/authorizations/error.html.erb new file mode 100644 index 000000000..59cedf8f3 --- /dev/null +++ b/app/views/doorkeeper/authorizations/error.html.erb @@ -0,0 +1,6 @@ +<h2><%= t('doorkeeper.authorizations.error.title') %></h2> + +<p id="errorExplanation"><%= @pre_auth.error_response.body[:error_description] %></p> +<p><a href="javascript:history.back()"><%= l(:button_back) %></a></p> + +<% html_title t('doorkeeper.authorizations.error.title') %> diff --git a/app/views/doorkeeper/authorizations/new.html.erb b/app/views/doorkeeper/authorizations/new.html.erb new file mode 100644 index 000000000..898f2e645 --- /dev/null +++ b/app/views/doorkeeper/authorizations/new.html.erb @@ -0,0 +1,48 @@ +<%= title t('.title') %> + +<div class="warning"> +<p><strong><%=h @pre_auth.client.name %></strong></p> + +<p><%= raw t('.prompt', client_name: content_tag(:strong, class: "text-info") { @pre_auth.client.name }) %></p> + +<div class="oauth-permissions"> + <p><%= t('.able_to') %>:</p> + <ul> + <li><%= l :text_oauth_implicit_permissions %></li> + <% @pre_auth.scopes.each do |scope| %> + <% if scope == 'admin' %> + <li><%= l :label_oauth_permission_admin %></li> + <% else %> + <li><%= l_or_humanize(scope, prefix: 'permission_') %></li> + <% end %> + <% end %> + </ul> +</div> + +<% if @pre_auth.scopes.include?('admin') %> + <p><%= l :text_oauth_admin_permission_info %></p> +<% end %> +</div> + +<p> + <%= form_tag oauth_authorization_path, method: :post do %> + <%= hidden_field_tag :client_id, @pre_auth.client.uid %> + <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %> + <%= hidden_field_tag :state, @pre_auth.state %> + <%= hidden_field_tag :response_type, @pre_auth.response_type %> + <%= hidden_field_tag :scope, @pre_auth.scope %> + <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge %> + <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method %> + <%= submit_tag t('doorkeeper.authorizations.buttons.authorize') %> + <% end %> + <%= form_tag oauth_authorization_path, method: :delete do %> + <%= hidden_field_tag :client_id, @pre_auth.client.uid %> + <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %> + <%= hidden_field_tag :state, @pre_auth.state %> + <%= hidden_field_tag :response_type, @pre_auth.response_type %> + <%= hidden_field_tag :scope, @pre_auth.scope %> + <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge %> + <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method %> + <%= submit_tag t('doorkeeper.authorizations.buttons.deny') %> + <% end %> +</p> diff --git a/app/views/doorkeeper/authorizations/show.html.erb b/app/views/doorkeeper/authorizations/show.html.erb new file mode 100644 index 000000000..25ee88a87 --- /dev/null +++ b/app/views/doorkeeper/authorizations/show.html.erb @@ -0,0 +1,8 @@ +<%= title [l('label_oauth_authorized_application_plural'), oauth_authorized_applications_path] %> + +<fieldset class="tabular"><legend><%= l(:label_information_plural) %></legend> + <p> + <label><%= t('.title') %>:</label> + <code><%= params[:code] %></code> + </p> +</fieldset> diff --git a/app/views/doorkeeper/authorized_applications/index.html.erb b/app/views/doorkeeper/authorized_applications/index.html.erb new file mode 100644 index 000000000..0a1fc8a00 --- /dev/null +++ b/app/views/doorkeeper/authorized_applications/index.html.erb @@ -0,0 +1,31 @@ +<%= title [t(:label_my_account), my_account_path], l('label_oauth_authorized_application_plural') %> + +<% if @applications.any? %> +<div class="autoscroll"> +<table class="list"> + <thead><tr> + <th><%= t('doorkeeper.authorized_applications.index.application') %></th> + <th><%= t('doorkeeper.authorized_applications.index.created_at') %></th> + <th></th> + </tr></thead> + <tbody> + <% @applications.each do |application| %> + <tr id="application_<%= application.id %>" class="<%= cycle("odd", "even") %>"> + <td class="name"><span><%= application.name %></span></td> + <td ><%= format_date application.created_at %></td> + <td class="buttons"> + <%= link_to sprite_icon('del', t('doorkeeper.authorized_applications.buttons.revoke')), oauth_authorized_application_path(application), :data => {:confirm => t('doorkeeper.authorized_applications.confirmations.revoke')}, :method => :delete, :class => 'icon icon-del' %> + </td> + </tr> + <% end %> + </tbody> +</table> +</div> +<% else %> + <p class="nodata"><%= l(:label_no_data) %></p> +<% end %> + +<% content_for :sidebar do %> +<% @user = User.current %> +<%= render :partial => 'my/sidebar' %> +<% end %> diff --git a/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_common_mark.html.erb b/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_common_mark.html.erb index 486b96424..a650b2751 100644 --- a/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_common_mark.html.erb +++ b/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_common_mark.html.erb @@ -81,6 +81,14 @@ <th></th><td>HTML is <del>not</del> <u>allowed</u>.</td><td>HTML is <del>not</del> <u>allowed</u>.</td> </tr> +<tr><th colspan="3">Alerts <span class="more_info">(<a href="<%= help_wiki_syntax_path(:detailed, anchor: "16") %>" target="_blank">more</a>)</span></th></tr> +<tr><th></th><td>> [!NOTE]<br>> You can use alerts like [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], and [!CAUTION].</td><td> +<div class="markdown-alert markdown-alert-note"> +<p class="markdown-alert-title">Note</p> +<p>You can use alerts like [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], and [!CAUTION].</p> +</div> +</td></tr> + </table> <p><a href="<%= help_wiki_syntax_path(:detailed) %>" onclick="window.open('<%= help_wiki_syntax_path(:detailed) %>', '', ''); return false;">More Information</a></p> diff --git a/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_detailed_common_mark.html.erb b/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_detailed_common_mark.html.erb index a47a570f1..a74094460 100644 --- a/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_detailed_common_mark.html.erb +++ b/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_detailed_common_mark.html.erb @@ -27,6 +27,7 @@ <li><a href='#12'>Macros</a></li> <li><a href='#13'>Code highlighting</a></li> <li><a href='#15'>Raw HTML</a></li> + <li><a href='#16'>Alerts</a></li> </ul> <h2><a name="2" class="wiki-page"></a>Links</h2> @@ -359,8 +360,8 @@ It can be expanded by clicking a link. <p>The <strong>style</strong> attribute can be used in raw HTML to apply custom formatting. The following CSS properties are allowed:</p> <pre><code> color background-color - width - height + width min-width max-width + height min-height max-height padding padding-left padding-right padding-top padding-bottom margin margin-left margin-right margin-top margin-bottom border border-left border-right border-top border-bottom border-radius border-style border-collapse border-spacing @@ -369,5 +370,52 @@ It can be expanded by clicking a link. float </code></pre> + <h2><a name="16" class="wiki-page"></a>Alerts</h2> + + <p> + <dl> + <dt><code>NOTE</code></dt> + <dd> + <pre><code>> [!NOTE]<br>> Wiki page edits are preserved as history, allowing you to restore previous versions if needed.</code></pre> + <div class="markdown-alert markdown-alert-note"> + <p class="markdown-alert-title">Note</p> + <p>Wiki page edits are preserved as history, allowing you to restore previous versions if needed.</p> + </div> + </dd> + <dt><code>TIP</code></dt> + <dd> + <pre><code>> [!TIP]<br>> To quickly review the update history of an issue, use the "History" tab for convenient access.</code></pre> + <div class="markdown-alert markdown-alert-tip"> + <p class="markdown-alert-title">Tip</p> + <p>To quickly review the update history of an issue, use the "History" tab for convenient access.</p> + </div> + </dd> + <dt><code>WARNING</code></dt> + <dd> + <pre><code>> [!WARNING]<br>> Deleting an issue is a permanent action. Be certain it is truly necessary before proceeding.</code></pre> + <div class="markdown-alert markdown-alert-warning"> + <p class="markdown-alert-title">Warning</p> + <p>Deleting an issue is a permanent action. Be certain it is truly necessary before proceeding.</p> + </div> + </dd> + <dt><code>IMPORTANT</code></dt> + <dd> + <pre><code>> [!IMPORTANT]<br>> Changing role permissions can affect user access across all projects, so be mindful of potential impacts.</code></pre> + <div class="markdown-alert markdown-alert-important"> + <p class="markdown-alert-title">Important</p> + <p>Changing role permissions can affect user access across all projects, so be mindful of potential impacts.</p> + </div> + </dd> + <dt><code>CAUTION</code></dt> + <dd> + <pre><code>> [!CAUTION]<br>> When installing plugins, make sure to verify compatibility. Version differences can cause unexpected behavior.</code></pre> + <div class="markdown-alert markdown-alert-caution"> + <p class="markdown-alert-title">Caution</p> + <p>When installing plugins, make sure to verify compatibility. Version differences can cause unexpected behavior.</p> + </div> + </dd> + </dl> + </p> + </body> </html> diff --git a/app/views/issues/index.html.erb b/app/views/issues/index.html.erb index af2510827..70f0b740a 100644 --- a/app/views/issues/index.html.erb +++ b/app/views/issues/index.html.erb @@ -35,9 +35,9 @@ <% end %> <% other_formats_links do |f| %> - <%= f.link_to_with_query_parameters 'Atom', :key => User.current.atom_key %> <%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '350px'); return false;" %> <%= f.link_to_with_query_parameters 'PDF' %> + <%= f.link_to_with_query_parameters 'Atom', :key => User.current.atom_key %> <% end %> <div id="csv-export-options" style="display:none;"> diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb index 8f732032a..e41a91bb3 100644 --- a/app/views/issues/show.html.erb +++ b/app/views/issues/show.html.erb @@ -37,8 +37,18 @@ <%= assignee_avatar(@issue.assigned_to, :size => "22", :class => "gravatar-child") if @issue.assigned_to %> </div> -<div class="subject"> -<%= render_issue_subject_with_tree(@issue) %> +<div data-controller="sticky-issue-header"> + <div class="subject" data-sticky-issue-header-target="original"> + <%= render_issue_subject_with_tree(@issue) %> + </div> + <div id="sticky-issue-header" data-sticky-issue-header-target="stickyHeader" class="issue"> + <span class="issue-heading"><%= issue_heading(@issue) %>:</span> + <span class="subject"><%= @issue.subject %></span> + </div> +</div> + +<div class="reaction"> + <%= reaction_button @issue %> </div> <p class="author"> <%= authoring @issue.created_on, @issue.author %>. @@ -132,8 +142,8 @@ end %> <%= render partial: 'action_menu_edit' unless User.current.wants_comments_in_reverse_order? %> <% other_formats_links do |f| %> - <%= f.link_to 'Atom', :url => {:key => User.current.atom_key} %> <%= f.link_to 'PDF' %> + <%= f.link_to 'Atom', :url => {:key => User.current.atom_key} %> <% end %> <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %> diff --git a/app/views/messages/show.html.erb b/app/views/messages/show.html.erb index b265cc962..b62709afa 100644 --- a/app/views/messages/show.html.erb +++ b/app/views/messages/show.html.erb @@ -27,6 +27,9 @@ <h2><%= avatar(@topic.author) %><%= @topic.subject %></h2> <div class="message"> +<div class="reaction"> + <%= reaction_button @topic %> +</div> <p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p> <div id="message_topic_wiki" class="wiki"> <%= textilizable(@topic, :content) %> @@ -44,6 +47,7 @@ <% @replies.each do |message| %> <div class="message reply" id="<%= "message-#{message.id}" %>"> <div class="contextual"> + <%= reaction_button message %> <%= quote_reply( url_for(:action => 'quote', :id => message, :format => 'js'), "#message-#{message.id} .wiki", diff --git a/app/views/my/account.html.erb b/app/views/my/account.html.erb index c8706a5f5..95afbabac 100644 --- a/app/views/my/account.html.erb +++ b/app/views/my/account.html.erb @@ -1,6 +1,7 @@ <div class="contextual"> <%= additional_emails_link(@user) %> <%= link_to(sprite_icon('key', l(:button_change_password)), { :action => 'password'}, :class => 'icon icon-passwd') if @user.change_password_allowed? %> +<%= link_to(sprite_icon('apps', l('label_oauth_authorized_application_plural')), oauth_authorized_applications_path, :class => 'icon icon-applications') if Setting.rest_api_enabled? %> <%= call_hook(:view_my_account_contextual, :user => @user)%> </div> diff --git a/app/views/news/show.html.erb b/app/views/news/show.html.erb index d07a09eb7..601f12072 100644 --- a/app/views/news/show.html.erb +++ b/app/views/news/show.html.erb @@ -22,12 +22,17 @@ </div> <% end %> -<p><% unless @news.summary.blank? %><em><%= @news.summary %></em><br /><% end %> -<span class="author"><%= authoring @news.created_on, @news.author %></span></p> -<div class="wiki"> -<%= textilizable(@news, :description) %> +<div class="news"> + <div class="reaction"> + <%= reaction_button @news %> + </div> + <p><% unless @news.summary.blank? %><em><%= @news.summary %></em><br /><% end %> + <span class="author"><%= authoring @news.created_on, @news.author %></span></p> + <div class="wiki"> + <%= textilizable(@news, :description) %> + </div> + <%= link_to_attachments @news %> </div> -<%= link_to_attachments @news %> <br /> <div id="comments" style="margin-bottom:16px;"> @@ -38,6 +43,7 @@ <% @comments.each do |comment| %> <% next if comment.new_record? %> <div class="contextual"> + <%= reaction_button comment %> <%= link_to_if_authorized sprite_icon('del', l(:button_delete)), { :controller => 'comments', :action => 'destroy', :id => @news, :comment_id => comment}, :data => {:confirm => l(:text_are_you_sure)}, :method => :delete, :title => l(:button_delete), diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 3a1c047c9..875e736b1 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -31,10 +31,10 @@ <% end %> <% other_formats_links do |f| %> - <%= f.link_to 'Atom', :url => {:key => User.current.atom_key} %> <% if @query.display_type == 'list' %> <%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '350px'); return false;" %> <% end %> + <%= f.link_to 'Atom', :url => {:key => User.current.atom_key} %> <% end %> <% html_title(l(:label_project_plural)) -%> diff --git a/app/views/reactions/_replace_button.js.erb b/app/views/reactions/_replace_button.js.erb new file mode 100644 index 000000000..a5c923ea4 --- /dev/null +++ b/app/views/reactions/_replace_button.js.erb @@ -0,0 +1,7 @@ +(() => { + const button = $('[data-reaction-button-id=<%= reaction_id_for @object %>]'); + + removeHoverTooltips(button); + button.html($('<%=j reaction_button @object %>').children()); + setupHoverTooltips(button); +})(); diff --git a/app/views/reactions/create.js.erb b/app/views/reactions/create.js.erb new file mode 100644 index 000000000..20f3cc7ed --- /dev/null +++ b/app/views/reactions/create.js.erb @@ -0,0 +1 @@ +<%= render 'replace_button' %> diff --git a/app/views/reactions/destroy.js.erb b/app/views/reactions/destroy.js.erb new file mode 100644 index 000000000..20f3cc7ed --- /dev/null +++ b/app/views/reactions/destroy.js.erb @@ -0,0 +1 @@ +<%= render 'replace_button' %> diff --git a/app/views/settings/_display.html.erb b/app/views/settings/_display.html.erb index 62c53dfbb..3b2f95798 100644 --- a/app/views/settings/_display.html.erb +++ b/app/views/settings/_display.html.erb @@ -22,7 +22,12 @@ <p><%= setting_check_box :gravatar_enabled, :data => {:enables => '#settings_gravatar_default'} %> <em class="info"><%= t(:text_avatar_server_config_html, :url => Redmine::Configuration['avatar_server_url']) %></em></p> -<p><%= setting_select :gravatar_default, gravatar_default_setting_options, :blank => :label_none %></p> +<p> + <%= setting_select :gravatar_default, gravatar_default_setting_options, :blank => :label_none %> + <em class="<%= Setting.gravatar_default == "initials" ? "info" : "hidden" %>"> + <%= t(:text_setting_gravatar_default_initials_html) %> + </em> +</p> <p><%= setting_check_box :thumbnails_enabled, :data => {:enables => '#settings_thumbnails_size'} %></p> @@ -35,3 +40,18 @@ <%= submit_tag l(:button_save) %> <% end %> + +<%= javascript_tag do %> + $('#settings_gravatar_default').on('change', function(e){ + const gravatar_default = e.target.value; + const em = e.target.parentElement.getElementsByTagName('em')[0]; + + if (gravatar_default === 'initials') { + em.classList.remove('hidden'); + em.classList.add('info'); + } else { + em.classList.add('hidden'); + em.classList.remove('info'); + } + }); +<% end %>
\ No newline at end of file diff --git a/app/views/settings/_general.html.erb b/app/views/settings/_general.html.erb index 043067f18..44206b6c2 100644 --- a/app/views/settings/_general.html.erb +++ b/app/views/settings/_general.html.erb @@ -37,6 +37,8 @@ <p><%= setting_text_field :feeds_limit, :size => 6 %></p> +<p><%= setting_check_box :reactions_enabled %></p> + <%= call_hook(:view_settings_general_form) %> </div> diff --git a/app/views/timelog/index.html.erb b/app/views/timelog/index.html.erb index 55e2312b3..d9985e922 100644 --- a/app/views/timelog/index.html.erb +++ b/app/views/timelog/index.html.erb @@ -29,8 +29,8 @@ <span class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></span> <% other_formats_links do |f| %> - <%= f.link_to_with_query_parameters 'Atom', :key => User.current.atom_key %> <%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '330px'); return false;" %> + <%= f.link_to_with_query_parameters 'Atom', :key => User.current.atom_key %> <% end %> <div id="csv-export-options" style="display:none;"> diff --git a/app/views/users/show.api.rsb b/app/views/users/show.api.rsb index bf415795d..0681903b8 100644 --- a/app/views/users/show.api.rsb +++ b/app/views/users/show.api.rsb @@ -11,7 +11,7 @@ api.user do api.passwd_changed_on @user.passwd_changed_on api.avatar_url gravatar_url(@user.mail, {rating: nil, size: nil, default: Setting.gravatar_default}) if @user.mail && Setting.gravatar_enabled? api.twofa_scheme @user.twofa_scheme if User.current.admin? || (User.current == @user) - api.api_key @user.api_key if User.current.admin? || (User.current == @user) + api.api_key @user.api_key if (User.current.admin? || (User.current == @user && !User.current.authorized_by_oauth?)) api.status @user.status if User.current.admin? render_api_custom_values @user.visible_custom_field_values, api diff --git a/app/views/wiki/date_index.html.erb b/app/views/wiki/date_index.html.erb index 7ee5b467d..c8acf933c 100644 --- a/app/views/wiki/date_index.html.erb +++ b/app/views/wiki/date_index.html.erb @@ -29,11 +29,11 @@ <% unless @pages.empty? %> <% other_formats_links do |f| %> - <%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => @project, :show_wiki_edits => 1, :key => User.current.atom_key} %> <% if User.current.allowed_to?(:export_wiki_pages, @project) %> <%= f.link_to('PDF', :url => {:action => 'export', :format => 'pdf'}) %> <%= f.link_to('HTML', :url => {:action => 'export'}) %> <% end %> + <%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => @project, :show_wiki_edits => 1, :key => User.current.atom_key} %> <% end %> <% end %> diff --git a/app/views/wiki/index.html.erb b/app/views/wiki/index.html.erb index 3f6d05fc2..a4afcb28e 100644 --- a/app/views/wiki/index.html.erb +++ b/app/views/wiki/index.html.erb @@ -22,14 +22,14 @@ <% unless @pages.empty? %> <% other_formats_links do |f| %> - <%= f.link_to 'Atom', - :url => {:controller => 'activities', :action => 'index', - :id => @project, :show_wiki_edits => 1, - :key => User.current.atom_key} %> <% if User.current.allowed_to?(:export_wiki_pages, @project) %> <%= f.link_to('PDF', :url => {:action => 'export', :format => 'pdf'}) %> <%= f.link_to('HTML', :url => {:action => 'export'}) %> <% end %> + <%= f.link_to 'Atom', + :url => {:controller => 'activities', :action => 'index', + :id => @project, :show_wiki_edits => 1, + :key => User.current.atom_key} %> <% end %> <% end %> diff --git a/config/application.rb b/config/application.rb index 1beeb2db2..96c6f9fb4 100644 --- a/config/application.rb +++ b/config/application.rb @@ -59,6 +59,7 @@ module RedmineApp config.i18n.enforce_available_locales = true config.i18n.fallbacks = true config.i18n.default_locale = 'en' + config.i18n.available_locales = Dir[Rails.root / 'config' / 'locales' / '*.yml'].map { |f| File.basename(f, '.yml').to_sym } # Configure the default encoding used in templates for Ruby 1.9. config.encoding = "utf-8" diff --git a/config/boot.rb b/config/boot.rb index 7479b5aff..72ea699a5 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,5 +1,16 @@ # frozen_string_literal: true +# Rack 3.1.14 or later limits query parameters to 4096 by default, which +# prevents saving workflows with many statuses. +# Setting RACK_QUERY_PARSER_PARAMS_LIMIT to 65536 allows handling up to +# approximately 100 statuses. +# +# See also: +# - https://www.redmine.org/issues/42875 +# - https://github.com/rack/rack/blob/v3.1.16/README.md#configuration +# - https://github.com/rack/rack/blob/v3.1.16/lib/rack/query_parser.rb#L57 +ENV['RACK_QUERY_PARSER_PARAMS_LIMIT'] ||= '65536' + # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) diff --git a/config/database.yml.example b/config/database.yml.example index 6b0849602..9fdccac06 100644 --- a/config/database.yml.example +++ b/config/database.yml.example @@ -4,6 +4,8 @@ production: adapter: mysql2 + # You can also use "trilogy", an adapter for MySQL-compatible servers. + # adapter: trilogy database: redmine host: localhost username: root @@ -19,6 +21,7 @@ production: development: adapter: mysql2 + # adapter: trilogy database: redmine_development host: localhost username: root @@ -33,6 +36,7 @@ development: # Do not set this db to the same as development or production. test: adapter: mysql2 + # adapter: trilogy database: redmine_test host: localhost username: root diff --git a/config/icon_source.yml b/config/icon_source.yml index dc1803cdc..64f0a1d8c 100644 --- a/config/icon_source.yml +++ b/config/icon_source.yml @@ -221,4 +221,18 @@ - name: unwatch svg: eye-off - name: copy-pre-content - svg: clipboard
\ No newline at end of file + svg: clipboard +- name: thumb-up + svg: thumb-up +- name: thumb-up-filled + svg: thumb-up + style: filled +- name: alert-circle + svg: alert-circle +- name: bulb + svg: bulb +- name: message-report + svg: message-report +- name: quote-filled + svg: quote + style: filled
\ No newline at end of file diff --git a/config/initializers/30-redmine.rb b/config/initializers/30-redmine.rb index cf13cab20..16bcebec4 100644 --- a/config/initializers/30-redmine.rb +++ b/config/initializers/30-redmine.rb @@ -4,9 +4,7 @@ require 'redmine/configuration' require 'redmine/plugin_loader' Rails.application.config.to_prepare do - I18n.backend = Redmine::I18n::Backend.new - # Forces I18n to load available locales from the backend - I18n.config.available_locales = nil + I18n::Backend::Simple.include(I18n::Backend::Pluralization) # Use Nokogiri as XML backend instead of Rexml ActiveSupport::XmlMini.backend = 'Nokogiri' @@ -23,6 +21,72 @@ end Redmine::PluginLoader.load Rails.application.config.to_prepare do + Doorkeeper.configure do + orm :active_record + + # Issue access tokens with refresh token + use_refresh_token + + # Authorization Code expiration time (default: 10 minutes). + # + # authorization_code_expires_in 10.minutes + + # Access token expiration time (default: 2 hours). + # If you want to disable expiration, set this to `nil`. + # + # access_token_expires_in 2.hours + + # Hash access and refresh tokens before persisting them. + # https://doorkeeper.gitbook.io/guides/security/token-and-application-secrets + hash_token_secrets + + # Hash application secrets before persisting them. + hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt' + + # limit supported flows to Auth code + grant_flows ['authorization_code'] + + realm Redmine::Info.app_name + base_controller 'ApplicationController' + default_scopes(*Redmine::AccessControl.public_permissions.map(&:name)) + optional_scopes(*(Redmine::AccessControl.permissions.map(&:name) << :admin)) + + # Forbids creating/updating applications with arbitrary scopes that are + # not in configuration, i.e. +default_scopes+ or +optional_scopes+. + enforce_configured_scopes + + allow_token_introspection false + + # allow http loopback redirect URIs but require https for all others + force_ssl_in_redirect_uri { |uri| !%w[localhost 127.0.0.1 web localohst:8080].include?(uri.host) } + + # Specify what redirect URI's you want to block during Application creation. + forbid_redirect_uri { |uri| %w[data vbscript javascript].include?(uri.scheme.to_s.downcase) } + + resource_owner_authenticator do + if require_login + if Setting.rest_api_enabled? + User.current + else + deny_access + end + end + end + + admin_authenticator do |_routes| + if !Setting.rest_api_enabled? || !User.current.admin? + deny_access + end + end + end + + # Use Redmine standard layouts and helpers for Doorkeeper OAuth2 screens + Doorkeeper::ApplicationsController.layout "admin" + Doorkeeper::ApplicationsController.main_menu = false + Doorkeeper::AuthorizationsController.layout "base" + Doorkeeper::AuthorizedApplicationsController.layout "base" + Doorkeeper::AuthorizedApplicationsController.main_menu = false + default_paths = [] default_paths << Rails.root.join("app/assets/javascripts") default_paths << Rails.root.join("app/assets/images") @@ -42,6 +106,14 @@ Rails.application.config.to_prepare do paths = theme.asset_paths Rails.application.config.assets.redmine_extension_paths << paths if paths.present? end + + Doorkeeper::ApplicationsController.class_eval do + require_sudo_mode :create, :show, :update, :destroy + end + + Doorkeeper::AuthorizationsController.class_eval do + require_sudo_mode :create, :destroy + end end Rails.application.deprecators[:redmine] = ActiveSupport::Deprecation.new('7.0', 'Redmine') diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb new file mode 100644 index 000000000..40888ad8b --- /dev/null +++ b/config/initializers/doorkeeper.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# rubocop:disable Lint/EmptyBlock +Doorkeeper.configure do +end + +Rails.application.config.to_prepare do +end +# rubocop:enable Lint/EmptyBlock diff --git a/config/locales/ar.yml b/config/locales/ar.yml index f52ec8dca..bff7a376a 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -1507,3 +1507,28 @@ ar: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/az.yml b/config/locales/az.yml index 3710566df..7bd2fffe2 100644 --- a/config/locales/az.yml +++ b/config/locales/az.yml @@ -1598,3 +1598,28 @@ az: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/bg.yml b/config/locales/bg.yml index 4f5b54ae3..67fcf82f5 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -211,6 +211,7 @@ bg: error_can_not_delete_custom_field: Невъзможност за изтриване на потребителско поле error_can_not_delete_tracker_html: Този тракер съдържа задачи и не може да бъде изтрит.<p>The following projects have issues with this tracker:<br>%{projects}</p> error_can_not_remove_role: Тази роля се използва и не може да бъде изтрита. + error_can_not_remove_role_reason_members_html: "<p>Следващите проекти имат членове с тази роля:<br>%{projects}</p>" error_can_not_reopen_issue_on_closed_version: Задача, асоциирана със затворена версия не може да бъде отворена отново error_can_not_archive_project: Този проект не може да бъде архивиран error_issue_done_ratios_not_updated: Процентът на завършените задачи не е обновен. @@ -239,6 +240,7 @@ bg: error_exceeds_maximum_hours_per_day: Не можете да запишете повече от %{max_hours} часа на един ден (%{logged_hours} часове вече са записани) error_can_not_delete_auth_source: Този режим за идентификация се използва и не може да бъде премахнат. error_spent_on_future_date: Не е възможно да се отчете изразходвано време на дата в бъдещето + error_spent_on_closed_issue: Не е възможно да се отчете изразходвано време за затворена задача error_not_allowed_to_log_time_for_other_users: Вие нямате разрешение да записвате изразходвано време за други потребители error_can_not_execute_macro_html: Грешка при изпълнение на <strong>%{name}</strong> макрос (%{error}) @@ -523,9 +525,13 @@ bg: setting_timelog_accept_0_hours: Приемане на записи с 0 часа setting_timelog_max_hours_per_day: Максимум часове, които могат да бъдат записани за ден и за потребител setting_timelog_accept_future_dates: Разрешено отчитане на изразходвано време на дата в бъдещето + setting_timelog_accept_closed_issues: Разрешено отчитане на изразходвано време за затворени задачи setting_show_status_changes_in_mail_subject: Показване на промените на състоянието на задачите в поле Относно на имейлите setting_project_list_defaults: Проектен списък setting_twofa: Двуфакторна автентикация + setting_related_issues_default_columns: Колони по подразбиране за свързани и подзадачи + setting_display_related_issues_table_headers: Показване на заглавия на таблиците + setting_reactions_enabled: Разрешаване на реакциите permission_add_project: Създаване на проект permission_add_subprojects: Създаване на подпроекти @@ -1339,6 +1345,7 @@ bg: Бие можете да го конфигурирате в config/configuration.yml. text_allowed_queries_to_select: Само публичните (за всички потребители) заявки са достъпни за избор text_setting_config_change: Вие можете да конфигурирате поведението в config/configuration.yml. Моля, рестартирайте Redmine след редактиране на файла. + text_setting_gravatar_default_initials_html: Инициалите на потребителите са изпратени на <a href="https://www.gravatar.com">https://www.gravatar.com</a> за генериране на техните аватари. default_role_manager: Мениджър default_role_developer: Разработчик @@ -1390,6 +1397,7 @@ bg: label_import_time_entries: Импортиране на записи за използвано време label_import_users: Импортиране на потребители sudo_mode_new_info_html: "<strong>Какво се случва?</strong> Трябва да потвърдите вашата парола преди да предприемете административни действия. Това осигурява вашият акаунт." + label_progressbar: Прогрес бар twofa__totp__name: Authenticator app twofa__totp__text_pairing_info_html: Сканирайте този QR код или въведете текстовия ключ @@ -1444,11 +1452,26 @@ bg: За да потвърдите, въведете името (%{login}) по-долу. text_project_destroy_enter_identifier: За да потвърдите действието, въведете идентификатора на проекта (%{identifier}) по-долу. field_name_or_email_or_login: Име, e-mail или login име - setting_wiki_tablesort_enabled: Javascript based table sorting in wiki content - label_progressbar: Progress bar - error_spent_on_closed_issue: Cannot log time on a closed issue - setting_timelog_accept_closed_issues: Accept time logs on closed issues - setting_related_issues_default_columns: Related and sub issues list defaults - setting_display_related_issues_table_headers: Show table headers - error_can_not_remove_role_reason_members_html: "<p>The following projects have members - with this role:<br>%{projects}</p>" + setting_wiki_tablesort_enabled: Сортиране на таблици чрез Javascript във wiki страници + reaction_text_x_other_users: + one: 1 друг + other: "%{count} други" + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/bs.yml b/config/locales/bs.yml index db44cf700..89c468f55 100644 --- a/config/locales/bs.yml +++ b/config/locales/bs.yml @@ -1493,3 +1493,28 @@ bs: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/ca.yml b/config/locales/ca.yml index a52b03b1a..456a1e9df 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -1494,3 +1494,28 @@ ca: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 45ed70711..2a80d379a 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -1489,3 +1489,28 @@ cs: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/da.yml b/config/locales/da.yml index 05c9d4c3d..8bbf9a9ba 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -1524,3 +1524,28 @@ da: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/de.yml b/config/locales/de.yml index 9ad00adfa..36e52fb3a 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -971,6 +971,9 @@ de: permission_view_time_entries: Gebuchte Aufwände ansehen permission_view_wiki_edits: Wiki-Versionsgeschichte ansehen permission_view_wiki_pages: Wiki ansehen + permission_view_project: Projekte ansehen + permission_search_project: Projekte suchen + permission_view_members: Projektmitglieder anzeigen project_module_boards: Foren project_module_calendar: Kalender @@ -1471,3 +1474,18 @@ de: setting_timelog_accept_closed_issues: Accept time logs on closed issues error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + label_oauth_permission_admin: Administrator-Zugriff + label_oauth_admin_access: Administration + label_oauth_application_plural: Applikationen + label_oauth_authorized_application_plural: Autorisierte Applikationen + text_oauth_admin_permission: Voller Admin-Zugriff. Wenn diese Applikation durch einen Administrator autorisiert wird, kann sie alle Daten lesen und schreiben, auch im Namen anderer Benutzer. + text_oauth_admin_permission_info: Diese Applikation verlangt vollen Administrator-Zugriff. Wenn Sie ein Administrator sind (oder in Zukunft Administrator werden), wird sie in der Lage sein, alle Daten zu lesen und zu schreiben, auch im Namen anderer Benutzer. Dies kann vermieden werden, indem die Applikation mit einem anderen Benutzerkonto ohne Administrator-Privileg autorisiert wird. + text_oauth_copy_secret_now: Das Geheimnis bitte jetzt an einen sicheren Ort kopieren, es kann nicht erneut angezeigt werden. + text_oauth_implicit_permissions: Zugriff auf Benutzername, Login sowie auf die primäre Email-Adresse + text_oauth_info_scopes: Scopes für die Applikation auswählen. Die Applikation wird niemals mehr Rechte haben als hier ausgewählt. Sie wird außerdem auf die Rollen und Projektmitgliedschaften des Benutzers, der sie autorisiert hat, beschränkt sein.
\ No newline at end of file diff --git a/config/locales/el.yml b/config/locales/el.yml index a92609e11..e2f9049f5 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -1507,3 +1507,28 @@ el: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index 79fd6478d..59ee096c4 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -1508,3 +1508,28 @@ en-GB: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/en.yml b/config/locales/en.yml index 819846e1a..947a8642f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -139,6 +139,9 @@ en: must_contain_special_chars: "must contain special characters (!, $, %, ...)" domain_not_allowed: "contains a domain not allowed (%{domain})" too_simple: "is too simple" + attributes: + doorkeeper/application: + scopes: Scopes actionview_instancetag_blank_option: Please select @@ -528,6 +531,7 @@ en: setting_twofa: Two-factor authentication setting_related_issues_default_columns: Related and sub issues list defaults setting_display_related_issues_table_headers: Show table headers + setting_reactions_enabled: Enable reactions permission_add_project: Create project permission_add_subprojects: Create subprojects @@ -604,6 +608,10 @@ en: permission_manage_related_issues: Manage related issues permission_import_issues: Import issues permission_log_time_for_other_users: Log spent time for other users + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + project_module_issue_tracking: Issue tracking project_module_time_tracking: Time tracking @@ -1157,6 +1165,10 @@ en: label_time_by_author: "%{time} by %{author}" label_involved_principals: Author / Previous assignee label_progressbar: Progress bar + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications button_login: Login button_submit: Submit @@ -1341,6 +1353,12 @@ en: text_no_subject: no subject text_allowed_queries_to_select: Public (to any users) queries only selectable text_setting_config_change: You can configure the behaviour in config/configuration.yml. Please restart the application after editing it. + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> to generate their avatars. + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. If you are an Administrator (or become one in the future), it will be able to read and write all data and impersonate other users on your behalf. If you want to avoid this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application will not be allowed to do more than what is selected here. It will also always be limited by the roles and project memberships of the user who authorized it. default_role_manager: Manager default_role_developer: Developer @@ -1432,3 +1450,6 @@ en: text_project_destroy_enter_identifier: "To confirm, please enter the project's identifier (%{identifier}) below." field_name_or_email_or_login: Name, email or login setting_wiki_tablesort_enabled: Javascript based table sorting in wiki content + reaction_text_x_other_users: + one: "1 other" + other: "%{count} others" diff --git a/config/locales/es-PA.yml b/config/locales/es-PA.yml index 8aef90a2d..a3c8c03df 100644 --- a/config/locales/es-PA.yml +++ b/config/locales/es-PA.yml @@ -1538,3 +1538,28 @@ es-PA: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/es.yml b/config/locales/es.yml index fd41f275b..ff46a4a21 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1574,3 +1574,28 @@ es: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/et.yml b/config/locales/et.yml index 467fcdcb1..d39c6d603 100644 --- a/config/locales/et.yml +++ b/config/locales/et.yml @@ -1512,3 +1512,28 @@ et: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/eu.yml b/config/locales/eu.yml index 40e6ef21e..bc59806d8 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -1508,3 +1508,28 @@ eu: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/fa.yml b/config/locales/fa.yml index b124cbff9..2b75de439 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -141,7 +141,7 @@ fa: must_contain_digits: "باید شامل عدد باشد (0-9)" must_contain_special_chars: "باید شامل نویسههای خاص باشد (!, $, %, ...)" domain_not_allowed: "شامل دامنهی غیرمجازست (%{domain})" - too_simple: "is too simple" + too_simple: "خیلی ساده است" actionview_instancetag_blank_option: انتخاب کنید @@ -275,10 +275,9 @@ fa: mail_body_security_notification_notify_disabled: "نشانی رایانامه %{value} دیگر آگاهسازی دریافت نخواهد کرد." mail_body_settings_updated: "تنظیمات زیر تغییر کردند:" mail_body_password_updated: "گذرواژه شما تغییر کرد." - mail_destroy_project_failed: Project %{value} could not be deleted. - mail_destroy_project_successful: Project %{value} was deleted successfully. - mail_destroy_project_with_subprojects_successful: Project %{value} and its subprojects - were deleted successfully. + mail_destroy_project_failed: پروژه %{value} نمیتواند حذف شود. + mail_destroy_project_successful: پروژه %{value} با موفقیت حذف شد. + mail_destroy_project_with_subprojects_successful: پروژه %{value} و زیرپروژههای آن با موفقیت حذف شدند. field_name: نام field_description: توضیح @@ -422,6 +421,10 @@ fa: field_default_issue_query: جستار پیشفرض مسئلهها field_default_project_query: جستار پیشفرض پروژه field_default_time_entry_activity: دستهبندی پیشفرض زمان صرف شده + field_any_searchable: هر متن قابل جستجویی + field_estimated_remaining_hours: باقیمانده زمان برآورد شده + field_last_activity_date: آخرین فعالیت + field_thousands_delimiter: جداکننده هزارگان setting_app_title: عنوان برنامه setting_welcome_text: متن خوشآمد گویی @@ -479,6 +482,7 @@ fa: setting_default_projects_modules: ابزارهای پیشفرض فعال برای پروژههای جدید setting_issue_done_ratio: برآورد اندازه انجام شده مسئله با setting_issue_done_ratio_issue_field: محاسبه بر اساس درصد پیشرفت مسئله + setting_issue_done_ratio_interval: فاصله گزینههای درصد انجام setting_issue_done_ratio_issue_status: محاسبه بر اساس وضعیت مسئله setting_start_of_week: آغاز تقویم از setting_rest_api_enabled: فعالسازی وب سرویسهای REST @@ -503,6 +507,7 @@ fa: setting_force_default_language_for_anonymous: الزام زبان پیش فرض برای کاربران ناشناس setting_force_default_language_for_loggedin: الزام زبان پیشفرض برای کاربران واردشده setting_link_copied_issue: ارتباط مسائل در هنگام رونوشت + setting_copy_attachments_on_issue_copy: رونوشت پیوستها در زمان رونوشت setting_max_additional_emails: بیشینه تعداد نشانیهای رایانامه setting_email_domains_allowed: دامنههای مجاز برای نشانی رایانامه setting_email_domains_denied: دامنههای غیرمجاز برای نشانی رایانامه @@ -526,6 +531,7 @@ fa: permission_edit_project: ویرایش پروژه permission_close_project: بستن / بازگشایی پروژه permission_delete_project: حذف پروژه + permission_select_project_publicity: تنظیم پروژه به عمومی یا محرمانه permission_select_project_modules: انتخاب ابزارهای پروژه permission_manage_members: مدیریت اعضا permission_manage_project_activities: مدیریت دستهبندی زمانهای پروژه @@ -632,6 +638,7 @@ fa: label_issue_assigned_to_updated: مسئول بهروز شد label_issue_priority_updated: اولویت بهروز شد label_issue_fixed_version_updated: نسخه هدف بهروز شد + label_issue_attachment_added: پیوست اضافه شد label_document: سند label_document_new: سند جدید label_document_plural: اسناد @@ -713,6 +720,10 @@ fa: label_attachment_plural: پیوستها label_file_added: پرونده افزوده شد label_attachment_description: توضیحات پیوست + label_attachment_summary: + zero: "%{filename}" + one: "%{filename} و 1 پرونده" + other: "%{filename} و %{count} پرونده" label_report: گزارش label_report_plural: گزارشها label_news: مطلب @@ -812,6 +823,7 @@ fa: label_more_than_ago: بعد از چند روز پیش label_ago: روز قبل label_contains: شامل + label_contains_any_of: شامل یکی از label_not_contains: فاقد label_starts_with: شروع با label_ends_with: پایان با @@ -820,6 +832,9 @@ fa: label_no_issues_in_project: مسئلهای در پروژه وجود ندارد label_any_open_issues: هر مسئلهی باز label_no_open_issues: بدون هیچ مسئله باز + label_has_been: بوده است + label_has_never_been: هرگز نبوده است + label_changed_from: تغییر کرده از label_day_plural: روز label_repository: مخزن label_repository_new: مخزن جدید @@ -839,6 +854,7 @@ fa: label_latest_revision_plural: آخرین بازبینیها label_view_revisions: مشاهده بازبینیها label_view_all_revisions: مشاهده همه بازبینیها + label_view_previous_annotation: مشاهده حاشیهنویسی قبل از این تغییر label_x_revisions: "%{count} بازبینی" label_max_size: بیشترین اندازه label_roadmap: نقشه راه @@ -964,6 +980,7 @@ fa: label_optional_description: توضیح اختیاری label_add_another_file: افزودن پرونده دیگر label_auto_watch_on: ناظر خودکار + label_auto_watch_on_issue_created: مسئلههایی که من ساختهام label_auto_watch_on_issue_contributed_to: مسئلههایی که در آنها مشارکت داشتهام label_preferences: ترجیحها label_chronological_order: به ترتیب تاریخ @@ -1116,6 +1133,8 @@ fa: label_inherited_from_group: "ارثبری از گروه %{name}" label_trackers_description: توضیحات انواع مسئله label_open_trackers_description: نمایش توضیحات تمامی انواع مسئله + label_issue_statuses_description: توضیحات وضعیتهای مسئله + label_open_issue_statuses_description: دیدن توضیحات همه وضعیتهای مسئلهها label_preferred_body_part_text: متن label_preferred_body_part_html: HTML label_issue_history_properties: تغییرات بخشها @@ -1133,6 +1152,7 @@ fa: label_default_query: جستار پیشفرض label_edited: ویرایش شده label_time_by_author: "%{time} توسط %{author}" + label_involved_principals: نویسنده / مسئول قبلی button_login: ورود button_submit: ثبت @@ -1198,6 +1218,7 @@ fa: button_edit_object: ویرایش %{object_name} button_delete_object: حذف %{object_name} button_create_and_follow: ساخت و برگشت + button_apply_issues_filter: اعمال غربال مسئلهها status_active: فعال status_registered: ثبتنامشده @@ -1274,6 +1295,7 @@ fa: text_minimagick_available: MiniMagick در دسترس است (اختیاری) text_convert_available: تبدیل ImageMagick در دسترس است (اختیاری) text_gs_available: پشتیبانی ImageMagick PDF موجود است (اختیاری) + text_default_active_job_queue_changed: آداپتور پیشفرض صف که به شکل مناسبی کار میکند تنها برای توسعه/آزمون تغییر کرد text_destroy_time_entries_question: "روی مسئلههایی که میخواهید حذف کنید، %{hours} ساعت زمان ثبت شده است. میخواهید چهکار کنید؟" text_destroy_time_entries: ساعتهای ثبت شده حذف شوند text_assign_time_entries_to_project: ساعتهای ثبت شده به پروژه واگذار شوند @@ -1310,11 +1332,11 @@ fa: text_project_closed: این پروژه بسته و فقطخواندنی است. text_turning_multiple_off: "اگر این گزینه را غیرفعال کنید، در بخشهای سفارشیِ ذخیرهشده، فقط یک مقدار باقی خواهد ماند و بقیه موارد حذف خواهند شد." text_select_apply_tracker: "انتخاب نوع مسئله" + text_select_apply_issue_status: "وضعیت مسئله را انتخا کنید" text_avatar_server_config_html: خادم فعلی آواتار <a href="%{url}">%{url}</a> میباشد. شما میتوانید آن را در config/configuration.yml تغییر دهید. text_no_subject: بدون موضوع text_allowed_queries_to_select: تنها جستارهای عمومی (برای همه کاربران) قابل انتخاب است - text_setting_config_change: میتوانید رفتار را در config/configuration.yml ویرایش کنید. - لطفا پس از تغییر، برنامه را دوباره اجرا کنید. + text_setting_config_change: میتوانید رفتار را در config/configuration.yml ویرایش کنید. لطفا پس از تغییر، برنامه را دوباره اجرا کنید. default_role_manager: مدیر default_role_developer: برنامهنویس @@ -1365,22 +1387,18 @@ fa: label_import_time_entries: ورود زمانها label_import_users: وارد کردن فهرست کاربران sudo_mode_new_info_html: "<strong>چه اتفاقی میافتد؟</strong> قبل از انجام هرگونه اقداماتِ راهبری باید گذرواژه خود را دوباره وارد کنید، این کار تضمین میکند که حساب شما محافظت شود." + twofa__totp__name: برنامک احراز هویت - twofa__totp__text_pairing_info_html: لطفا کد QR را را در یک برنامه TOTP اسکن کنید یا کلید متنی را در آن وارد کنید (به عنوان مثال <a href="https://support.google.com/accounts/answer/1066447"> Google - Authenticator </a>، <a href="https://authy.com/download/"> Authy </a>، <a href = "https://guide.duo.com/third-party-accounts" > Duo - Mobile </a>) سپس کدی که برنامه در اختیار شما میگذارد را در قسمت زیر وارد نمایید تا احراز هویت دو مرحله ای فعال شود. + twofa__totp__text_pairing_info_html: لطفا کد QR را را در یک برنامه TOTP اسکن کنید یا کلید متنی را در آن وارد کنید (به عنوان مثال <a href="https://support.google.com/accounts/answer/1066447"> Google Authenticator </a>، <a href="https://authy.com/download/"> Authy </a>، <a href = "https://guide.duo.com/third-party-accounts" > Duo Mobile </a>) سپس کدی که برنامه در اختیار شما میگذارد را در قسمت زیر وارد نمایید تا احراز هویت دو مرحله ای فعال شود. twofa__totp__label_plain_text_key: کلید متنی twofa__totp__label_activate: فعالسازی برنامک احراز هویت twofa_currently_active: 'فعال: %{twofa_scheme_name}' twofa_not_active: فعال نشده است twofa_label_code: کد twofa_hint_disabled_html: تنظیم <strong>%{label}</strong> احراز هویت دو عاملی را غیرفعال کرده و اتصال همه دستگاههای تمامی کاربران را قطع میکند. - twofa_hint_optional_html: تنظیم <strong>%{label}</strong> به کاربران اجازه میدهد - احراز هویت دوعاملی را در صورت تمایل فعال کنند، مگر اینکه توسط یکی از گروههای کاربری الزامی شده باشد. + twofa_hint_optional_html: تنظیم <strong>%{label}</strong> به کاربران اجازه میدهد احراز هویت دوعاملی را در صورت تمایل فعال کنند، مگر اینکه توسط یکی از گروههای کاربری الزامی شده باشد. twofa_hint_required_html: تنظیم <strong>%{label}</strong> تمامی کاربران را ملزم میکند تا احراز هویت دو عاملی را در اولین ورود بعدی فعال کنند. - twofa_hint_required_administrators_html: تنظیم <strong>%{label}</strong> شبیه - احراز هویت دوعاملی اختیاری عمل میکند, اما برای راهبرها احراز هویت دوعاملی را - در اولین ورود بعدی الزامی میکند. + twofa_hint_required_administrators_html: تنظیم <strong>%{label}</strong> شبیه احراز هویت دوعاملی اختیاری عمل میکند, اما برای راهبرها احراز هویت دوعاملی را در اولین ورود بعدی الزامی میکند. twofa_label_setup: فعالسازی احراز هویت دو عاملی twofa_label_deactivation_confirmation: غیرفعالسازی احراز هویت دو عاملی twofa_notice_select: 'لطفاً طرح دو عاملی را که می خواهید استفاده کنید انتخاب کنید:' @@ -1408,31 +1426,7 @@ fa: twofa_text_group_disabled: "این تنظیم تنها زمانی مؤثر است که احراز هویت دوعاملی روی «اختیاری» تنظیم شده باشد. در حال حاضر، احراز هویت دوعاملی غیرفعال است." text_user_destroy_confirmation: مطمئنید که می خواهید این کاربر و همه ارجاعهای مربوط به او را حذف کنید؟ این کار قابل بازگشت نیست. غالبا، قفل کردن کاربر به جای حذف، راه حل بهتری است. برای تأیید، لطفاً شناسه ورود کاربر (%{login}) را در زیر وارد کنید. text_project_destroy_enter_identifier: برای تایید، لطفا شناسه پروژه (%{identifier}) را وارد کنید. - permission_select_project_publicity: تنظیم عمومی یا خصوصی بودن پروژه - label_auto_watch_on_issue_created: مسئلههایی که من ساختهام - field_any_searchable: هر متن قابل جستجویی - label_contains_any_of: شامل یکی از - button_apply_issues_filter: اعمال غربال مسئلهها - label_view_previous_annotation: مشاهده حاشیهنویسی قبل از این تغییر - label_has_been: بوده است - label_has_never_been: نبوده است - label_changed_from: تغییر یافته از - label_issue_statuses_description: توضیحات وضعیتهای مسئله - label_open_issue_statuses_description: مشاهده توضیحات همه وضعیتهای مسئلهها - text_select_apply_issue_status: وضعیت مسئله را انتخاب کنید field_name_or_email_or_login: نام، رایانامه یا شناسه کاربری - text_default_active_job_queue_changed: آداپتور پیشفرض صف که به شکل مناسبی کار میکند تنها برای توسعه/آزمون تغییر کرد - label_issue_attachment_added: Attachment added - field_estimated_remaining_hours: Estimated remaining time - field_last_activity_date: Last activity - setting_issue_done_ratio_interval: Done ratio options interval - setting_copy_attachments_on_issue_copy: Copy attachments on copy - field_thousands_delimiter: Thousands delimiter - label_involved_principals: Author / Previous assignee - label_attachment_summary: - zero: "%{filename}" - one: "%{filename} and 1 file" - other: "%{filename} and %{count} files" setting_wiki_tablesort_enabled: Javascript based table sorting in wiki content label_progressbar: Progress bar error_spent_on_closed_issue: Cannot log time on a closed issue @@ -1441,3 +1435,28 @@ fa: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/fi.yml b/config/locales/fi.yml index c6c90f1dc..d8a62af1c 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -1525,3 +1525,28 @@ fi: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 7a072e32f..26aa88ac6 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1484,3 +1484,28 @@ fr: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 51e5ed99e..413352c9a 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -1513,3 +1513,28 @@ gl: setting_display_related_issues_table_headers: Amosar as cabeceiras da táboa error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/he.yml b/config/locales/he.yml index 26c37fff6..f9362290c 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -1512,3 +1512,28 @@ he: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/hr.yml b/config/locales/hr.yml index 83af380f0..d024c3a11 100644 --- a/config/locales/hr.yml +++ b/config/locales/hr.yml @@ -1504,3 +1504,28 @@ hr: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/hu.yml b/config/locales/hu.yml index b4a864826..6294990b5 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -1496,3 +1496,28 @@ setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/id.yml b/config/locales/id.yml index 21291a994..cf273c65e 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -1509,3 +1509,28 @@ id: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/it.yml b/config/locales/it.yml index 66d084cdc..21942da44 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1446,3 +1446,28 @@ it: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/ja.yml b/config/locales/ja.yml index d72d136ec..2019f4272 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1450,10 +1450,34 @@ ja: label_attachment_summary: zero: "%{filename}" other: "%{filename} ほか%{count}件" - setting_wiki_tablesort_enabled: Javascript based table sorting in wiki content + setting_wiki_tablesort_enabled: コンテンツ内テーブルの Tablesort (JavaScript) による並べ替え label_progressbar: 進捗バー error_spent_on_closed_issue: 完了したチケットに作業時間を記録することはできません setting_timelog_accept_closed_issues: 完了したチケットへの作業時間の記録を許可 setting_related_issues_default_columns: 関連するチケットと子チケットの一覧で表示する項目 setting_display_related_issues_table_headers: テーブルヘッダを表示 error_can_not_remove_role_reason_members_html: "<p>以下のプロジェクトにこのロールのメンバーがいます:<br>%{projects}</p>" + setting_reactions_enabled: リアクション機能を有効にする + reaction_text_x_other_users: + one: 他1人 + other: "他%{count}人" + text_setting_gravatar_default_initials_html: ユーザーの姓と名それぞれの先頭文字がアイコン生成のために <a href="https://www.gravatar.com">https://www.gravatar.com</a> に送信されます。 + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/ko.yml b/config/locales/ko.yml index f2c7fa7d7..78ee45897 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -1524,3 +1524,28 @@ ko: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 30998b050..200f7ae3c 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -1468,3 +1468,28 @@ lt: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/lv.yml b/config/locales/lv.yml index 1d43e621d..8f4d00a8f 100644 --- a/config/locales/lv.yml +++ b/config/locales/lv.yml @@ -1501,3 +1501,28 @@ lv: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/mk.yml b/config/locales/mk.yml index ee950d96e..7e58bcab7 100644 --- a/config/locales/mk.yml +++ b/config/locales/mk.yml @@ -1507,3 +1507,28 @@ mk: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/mn.yml b/config/locales/mn.yml index af4c0a5ec..38ee6b463 100644 --- a/config/locales/mn.yml +++ b/config/locales/mn.yml @@ -1507,3 +1507,28 @@ mn: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 18644e021..41d6a894a 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1482,3 +1482,28 @@ nl: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/no.yml b/config/locales/no.yml index a0a08e825..b4b66a860 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -1498,3 +1498,28 @@ setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 4c43abc0b..1ad94fcc3 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -1451,3 +1451,28 @@ pl: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 1402dcfa1..60a1103af 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -1512,3 +1512,28 @@ pt-BR: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/pt.yml b/config/locales/pt.yml index e92bb4bd3..4cd941f41 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -1500,3 +1500,28 @@ pt: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/ro.yml b/config/locales/ro.yml index 38f23ce63..2b98c7ed5 100644 --- a/config/locales/ro.yml +++ b/config/locales/ro.yml @@ -1502,3 +1502,28 @@ ro: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 540d961c4..1f02f2979 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -1576,3 +1576,28 @@ ru: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 2a35d0537..bd3cd0a13 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -1496,3 +1496,28 @@ sk: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/sl.yml b/config/locales/sl.yml index 9c51b232c..e47e355f5 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -1507,3 +1507,28 @@ sl: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/sq.yml b/config/locales/sq.yml index f7b3e4fbd..73999469c 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -1469,3 +1469,28 @@ sq: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/sr-YU.yml b/config/locales/sr-YU.yml index 5d83fa6e2..f0d82e9c6 100644 --- a/config/locales/sr-YU.yml +++ b/config/locales/sr-YU.yml @@ -1509,3 +1509,28 @@ sr-YU: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/sr.yml b/config/locales/sr.yml index baeb0ad21..cb47ba239 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -1508,3 +1508,28 @@ sr: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 607c43427..18fdbdfa3 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -2,6 +2,7 @@ # by Johan Lundström (johanlunds@gmail.com), # with parts taken from http://github.com/daniel/swe_rails # Update based on Redmine 2.6.0 by Khedron Wilk (khedron.wilk@gmail.com) 6th Dec 2014 +# Update based on Redmine 6.0.1 by Jimmy 'Grovkillen' Westberg (jimmy@grovkillen.com) 5th Dec 2024 sv: number: # Used in number_with_delimiter() @@ -135,14 +136,14 @@ sv: circular_dependency: "Denna relation skulle skapa ett cirkulärt beroende" cant_link_an_issue_with_a_descendant: "Ett ärende kan inte länkas till ett av dess underärenden" earlier_than_minimum_start_date: "kan inte vara tidigare än %{date} på grund av föregående ärenden" - not_a_regexp: "is not a valid regular expression" - open_issue_with_closed_parent: "An open issue cannot be attached to a closed parent task" - must_contain_uppercase: "must contain uppercase letters (A-Z)" - must_contain_lowercase: "must contain lowercase letters (a-z)" - must_contain_digits: "must contain digits (0-9)" - must_contain_special_chars: "must contain special characters (!, $, %, ...)" - domain_not_allowed: "contains a domain not allowed (%{domain})" - too_simple: "is too simple" + not_a_regexp: "är inte korrekt formaterad RegEx" + open_issue_with_closed_parent: "Ett öppet ärende kan inte kopplas till ett stängt föräldraärende" + must_contain_uppercase: "måste innehålla versaler (A-Z)" + must_contain_lowercase: "måste innehålla gemener (a-z)" + must_contain_digits: "måste innehålla siffror (0-9)" + must_contain_special_chars: "måste innehålla specialtecken (!, $, %, ...)" + domain_not_allowed: "består av en ej godkänd domän (%{domain})" + too_simple: "är för simpel" direction: ltr date: @@ -178,9 +179,8 @@ sv: # Used in array.to_sentence. support: array: - last_word_connector: " och " - two_words_connector: " och " - words_connector: ", " + sentence_connector: "och" + skip_last_comma: true actionview_instancetag_blank_option: Var god välj @@ -217,16 +217,16 @@ sv: notice_feeds_access_key_reseted: Din Atom-nyckel återställdes. notice_api_access_key_reseted: Din API-nyckel återställdes. notice_failed_to_save_issues: "Misslyckades med att spara %{count} ärende(n) på %{total} valda: %{ids}." - notice_failed_to_save_time_entries: "Misslyckades med att spara %{count} tidloggning(ar) på %{total} valda: %{ids}." + notice_failed_to_save_time_entries: "Misslyckades med att spara %{count} tidsstämpling(ar) på %{total} valda: %{ids}." notice_failed_to_save_members: "Misslyckades med att spara medlem(mar): %{errors}." notice_account_pending: "Ditt konto skapades och avvaktar nu administratörens godkännande." notice_default_data_loaded: Standardkonfiguration inläst. notice_unable_delete_version: Denna version var inte möjlig att ta bort. - notice_unable_delete_time_entry: Tidloggning kunde inte tas bort. + notice_unable_delete_time_entry: Tidsstämplingen kunde inte tas bort. notice_issue_done_ratios_updated: "% klart uppdaterade." notice_gantt_chart_truncated: "Schemat förminskades eftersom det överskrider det maximala antalet aktiviteter som kan visas (%{max})" notice_issue_successful_create: "Ärende %{id} skapades." - notice_issue_update_conflict: "Detta ärende har uppdaterats av en annan användare samtidigt som du redigerade det." + notice_issue_update_conflict: "Detta ärende har uppdaterats av en annan användare under tiden du redigerade det." notice_account_deleted: "Ditt konto har avslutats permanent." notice_user_successful_create: "Användare %{id} skapad." @@ -237,9 +237,9 @@ sv: error_scm_annotate_big_text_file: Inlägget kan inte annoteras eftersom det överskrider maximal storlek för textfiler. error_issue_not_found_in_project: 'Ärendet hittades inte eller så tillhör det inte detta projekt' error_no_tracker_in_project: 'Ingen ärendetyp är associerad med projektet. Vänligen kontrollera projektinställningarna.' - error_no_default_issue_status: 'Ingen status är definierad som standard för nya ärenden. Vänligen kontrollera din konfiguration (Gå till "Administration -> Ärendestatus").' - error_can_not_delete_custom_field: Kan inte ta bort användardefinerat fält - error_can_not_delete_tracker_html: "Det finns ärenden av denna typ och den är därför inte möjlig att ta bort.<p>The following projects have issues with this tracker:<br>%{projects}</p>" + error_no_default_issue_status: 'Ingen status är definierad som standard för nya ärenden. Vänligen kontrollera din konfiguration (gå till "Administration -> Ärendestatus").' + error_can_not_delete_custom_field: Kan inte ta bort anpassat fält + error_can_not_delete_tracker_html: "Det finns ärenden av denna typ och den är därför inte möjlig att ta bort.<p>Följande projekt har ärenden kopplad till denna ärendetyp:<br>%{projects}</p>" error_can_not_remove_role: "Denna roll används och den är därför inte möjlig att ta bort." error_can_not_reopen_issue_on_closed_version: 'Ett ärende tilldelat en stängd version kan inte öppnas på nytt' error_can_not_archive_project: Detta projekt kan inte arkiveras @@ -267,7 +267,6 @@ sv: mail_subject_wiki_content_updated: "'%{id}' wikisida har uppdaterats" mail_body_wiki_content_updated: "The '%{id}' wikisida har uppdaterats av %{author}." - field_name: Namn field_description: Beskrivning field_summary: Sammanfattning @@ -278,14 +277,14 @@ sv: field_filename: Fil field_filesize: Storlek field_downloads: Nerladdningar - field_author: Författare + field_author: Skapare field_created_on: Skapad field_updated_on: Uppdaterad field_closed_on: Stängd field_field_format: Format field_is_for_all: För alla projekt field_possible_values: Möjliga värden - field_regexp: Reguljärt uttryck + field_regexp: Reguljärt uttryck (RegEx) field_min_length: Minimilängd field_max_length: Maxlängd field_value: Värde @@ -300,18 +299,18 @@ sv: field_tracker: Ärendetyp field_subject: Ämne field_due_date: Deadline - field_assigned_to: Tilldelad till + field_assigned_to: Tilldelad field_priority: Prioritet field_fixed_version: Versionsmål field_user: Användare - field_principal: User or Group + field_principal: Användare eller grupp field_role: Roll field_homepage: Hemsida field_is_public: Publik field_parent: Underprojekt till field_is_in_roadmap: Visa ärenden i roadmap field_login: Användarnamn - field_mail_notification: Mailnotifieringar + field_mail_notification: E-postaviseringar field_admin: Administratör field_last_login_on: Senaste inloggning field_language: Språk @@ -334,7 +333,7 @@ sv: field_done_ratio: "% Klart" field_auth_source: Autentiseringsläge field_hide_mail: Dölj min mailadress - field_comments: Kommentar + field_comments: Kommentarer field_url: URL field_start_page: Startsida field_subproject: Underprojekt @@ -347,7 +346,7 @@ sv: field_delay: Fördröjning field_assignable: Ärenden kan tilldelas denna roll field_redirect_existing_links: Omdirigera existerande länkar - field_estimated_hours: Estimerad tid + field_estimated_hours: Beräknad tid field_column_names: Kolumner field_time_entries: Spenderad tid field_time_zone: Tidszon @@ -399,7 +398,7 @@ sv: setting_feeds_limit: Innehållsgräns för Feed setting_default_projects_public: Nya projekt är publika setting_autofetch_changesets: Automatisk hämtning av commits - setting_sys_api_enabled: Aktivera WS för versionsarkivhantering + setting_sys_api_enabled: Aktivera API för versionsarkivhantering setting_commit_ref_keywords: Referens-nyckelord setting_commit_fix_keywords: Fix-nyckelord setting_autologin: Automatisk inloggning @@ -409,7 +408,7 @@ sv: setting_cross_project_issue_relations: Tillåt ärenderelationer mellan projekt setting_issue_list_default_columns: Standardkolumner i ärendelistan setting_repositories_encodings: Encoding för bilagor och versionsarkiv - setting_emails_header: Mail-header + setting_emails_header: Inledning setting_emails_footer: Signatur setting_protocol: Protokoll setting_per_page_options: Alternativ, objekt per sida @@ -418,7 +417,7 @@ sv: setting_display_subprojects_issues: Visa ärenden från underprojekt i huvudprojekt setting_enabled_scm: Aktivera SCM setting_mail_handler_body_delimiters: "Trunkera mail efter en av följande rader" - setting_mail_handler_api_enabled: Aktivera WS för inkommande mail + setting_mail_handler_api_enabled: Aktivera API för inkommande mail setting_mail_handler_api_key: API-nyckel setting_sequential_project_identifiers: Generera projektidentifierare sekventiellt setting_gravatar_enabled: Använd Gravatar-avatarer @@ -429,21 +428,21 @@ sv: setting_password_min_length: Minsta tillåtna lösenordslängd setting_new_project_user_role_id: Tilldelad roll för en icke-administratör som skapar ett projekt setting_default_projects_modules: Aktiverade moduler för nya projekt - setting_issue_done_ratio: Beräkna % klart med + setting_issue_done_ratio: Beräkna "% Klart" med setting_issue_done_ratio_issue_field: Använd ärendefältet setting_issue_done_ratio_issue_status: Använd ärendestatus setting_start_of_week: Första dagen i veckan - setting_rest_api_enabled: Aktivera REST webbtjänst - setting_cache_formatted_text: Cacha formaterad text - setting_default_notification_option: Standard notifieringsalternativ - setting_commit_logtime_enabled: Aktivera tidloggning - setting_commit_logtime_activity_id: Aktivitet för loggad tid - setting_gantt_items_limit: Maximalt antal aktiviteter som visas i gantt-schemat - setting_issue_group_assignment: Tillåt att ärenden tilldelas till grupper + setting_rest_api_enabled: Aktivera REST-API + setting_cache_formatted_text: Förladda formaterad text + setting_default_notification_option: Standardaviseringsalternativ + setting_commit_logtime_enabled: Aktivera tidsstämpling + setting_commit_logtime_activity_id: Aktivitet för stämplad tid + setting_gantt_items_limit: Maximalt antal aktiviteter som visas i Gantt-schemat + setting_issue_group_assignment: Tillåt att ärenden tilldelas grupper setting_default_issue_start_date_to_creation_date: Använd dagens datum som startdatum för nya ärenden - setting_commit_cross_project_ref: Tillåt ärende i alla de andra projekten att bli refererade och fixade + setting_commit_cross_project_ref: Tillåt ärenden i alla projekt att refereras och åtgärdas setting_unsubscribe: Tillåt användare att avsluta prenumereration - setting_session_lifetime: Maximal sessionslivslängd + setting_session_lifetime: Maximal sessionslängd setting_session_timeout: Tidsgräns för sessionsinaktivitet setting_thumbnails_enabled: Visa miniatyrbilder av bilagor setting_thumbnails_size: Storlek på miniatyrbilder (i pixlar) @@ -472,17 +471,17 @@ sv: permission_view_private_notes: Visa privata anteckningar permission_set_notes_private: Ställa in anteckningar som privata permission_delete_issues: Ta bort ärenden - permission_manage_public_queries: Hantera publika frågor - permission_save_queries: Spara frågor + permission_manage_public_queries: Hantera publika filtreringar + permission_save_queries: Spara filtreringar permission_view_gantt: Visa Gantt-schema permission_view_calendar: Visa kalender permission_view_issue_watchers: Visa bevakarlista permission_add_issue_watchers: Lägga till bevakare permission_delete_issue_watchers: Ta bort bevakare - permission_log_time: Logga spenderad tid + permission_log_time: Stämpla spenderad tid permission_view_time_entries: Visa spenderad tid - permission_edit_time_entries: Ändra tidloggningar - permission_edit_own_time_entries: Ändra egna tidloggningar + permission_edit_time_entries: Ändra tidsstämplingar + permission_edit_own_time_entries: Ändra egna tidsstämplingar permission_manage_news: Hantera nyheter permission_comment_news: Kommentera nyheter permission_view_documents: Visa dokument @@ -501,7 +500,7 @@ sv: permission_protect_wiki_pages: Skydda wikisidor permission_manage_repository: Hantera versionsarkiv permission_browse_repository: Bläddra i versionsarkiv - permission_view_changesets: Visa changesets + permission_view_changesets: Visa kodändringar permission_commit_access: Commit-åtkomst permission_manage_boards: Hantera forum permission_view_messages: Visa meddelanden @@ -571,10 +570,10 @@ sv: label_issue_category: Ärendekategori label_issue_category_plural: Ärendekategorier label_issue_category_new: Ny kategori - label_custom_field: Användardefinerat fält - label_custom_field_plural: Användardefinerade fält - label_custom_field_new: Nytt användardefinerat fält - label_enumerations: Uppräkningar + label_custom_field: Anpassat fält + label_custom_field_plural: Anpassade fält + label_custom_field_new: Nytt anpassat fält + label_enumerations: Värdelistor label_enumeration_new: Nytt värde label_information: Information label_information_plural: Information @@ -588,8 +587,8 @@ sv: label_login: Logga in label_logout: Logga ut label_help: Hjälp - label_reported_issues: Rapporterade ärenden - label_assigned_to_me_issues: Ärenden tilldelade till mig + label_reported_issues: Skapade ärenden + label_assigned_to_me_issues: Ärenden tilldelade mig label_registered_on: Registrerad label_activity: Aktivitet label_user_activity: "Aktiviteter för %{value}" @@ -668,7 +667,7 @@ sv: label_next: Nästa label_previous: Föregående label_used_by: Använd av - label_details: Detaljer + label_details: Sammanfattning label_add_note: Lägg till anteckning label_calendar: Kalender label_months_from: månader från @@ -685,12 +684,12 @@ sv: label_comment_add: Lägg till kommentar label_comment_added: Kommentar tillagd label_comment_delete: Ta bort kommentar - label_query: Användardefinerad fråga - label_query_plural: Användardefinerade frågor - label_query_new: Ny fråga - label_my_queries: Mina egna frågor - label_filter_add: Lägg till filter - label_filter_plural: Filter + label_query: Anpassad filtrering + label_query_plural: Anpassade filtreringar + label_query_new: Ny filtrering + label_my_queries: Mina egna filtreringar + label_filter_add: Lägg till filtreringsrad + label_filter_plural: Filtrering label_equals: är label_not_equals: är inte label_in_less_than: om mindre än @@ -739,7 +738,7 @@ sv: label_view_revisions: Visa revisioner label_view_all_revisions: Visa alla revisioner label_max_size: Maxstorlek - label_roadmap: Vägkarta + label_roadmap: Utvecklingsplan label_roadmap_due_in: "Färdig om %{value}" label_roadmap_overdue: "%{value} sen" label_roadmap_no_issues: Inga ärenden för denna version @@ -765,7 +764,7 @@ sv: label_change_plural: Ändringar label_statistics: Statistik label_commits_per_month: Commits per månad - label_commits_per_author: Commits per författare + label_commits_per_author: Commits per användare label_diff: skillnader label_view_diff: Visa skillnader label_diff_inline: i texten @@ -796,7 +795,7 @@ sv: label_board_new: Nytt forum label_board_plural: Forum label_board_locked: Låst - label_board_sticky: Klibbig + label_board_sticky: Fäst label_topic_plural: Ämnen label_message_plural: Meddelanden label_message_last: Senaste meddelande @@ -811,7 +810,7 @@ sv: label_date_to: Till label_language_based: Språkbaserad label_sort_by: "Sortera på %{value}" - label_send_test_email: Skicka testmail + label_send_test_email: Skicka ett testmejl label_feeds_access_key: Atom-nyckel label_missing_feeds_access_key: Saknar en Atom-nyckel label_feeds_access_key_created_on: "Atom-nyckel skapad för %{value} sedan" @@ -833,8 +832,8 @@ sv: label_user_mail_option_selected: "För alla händelser i markerade projekt..." label_user_mail_option_none: "Inga händelser" label_user_mail_option_only_my_events: "Endast för saker jag bevakar eller är inblandad i" - label_user_mail_no_self_notified: "Jag vill inte bli underrättad om ändringar som jag har gjort" - label_registration_activation_by_email: kontoaktivering med mail + label_user_mail_no_self_notified: "Jag vill inte bli underrättad om ändringar som jag själv gjort" + label_registration_activation_by_email: kontoaktivering med e-post label_registration_manual_activation: manuell kontoaktivering label_registration_automatic_activation: automatisk kontoaktivering label_display_per_page: "Per sida: %{value}" @@ -844,13 +843,13 @@ sv: label_scm: SCM label_plugins: Tillägg label_ldap_authentication: LDAP-autentisering - label_downloads_abbr: Nerl. + label_downloads_abbr: Nedl. label_optional_description: Valfri beskrivning label_add_another_file: Lägg till ytterligare en fil label_preferences: Användarinställningar label_chronological_order: I kronologisk ordning label_reverse_chronological_order: I omvänd kronologisk ordning - label_incoming_emails: Inkommande mail + label_incoming_emails: Inkommande e-post label_generate_key: Generera en nyckel label_issue_watchers: Bevakare label_example: Exempel @@ -880,14 +879,14 @@ sv: label_api_access_key_created_on: "API-nyckel skapad för %{value} sedan" label_profile: Profil label_subtask_plural: Underaktiviteter - label_project_copy_notifications: Skicka mailnotifieringar när projektet kopieras + label_project_copy_notifications: Skicka e-postaviseringar när projektet kopieras label_principal_search: "Sök efter användare eller grupp:" label_user_search: "Sök efter användare:" label_additional_workflow_transitions_for_author: Ytterligare övergångar tillåtna när användaren är den som skapat ärendet label_additional_workflow_transitions_for_assignee: Ytterligare övergångar tillåtna när användaren är den som tilldelats ärendet label_issues_visibility_all: Alla ärenden label_issues_visibility_public: Alla icke-privata ärenden - label_issues_visibility_own: Ärenden skapade av eller tilldelade till användaren + label_issues_visibility_own: Ärenden skapade av eller tilldelade användaren label_git_report_last_commit: Rapportera senaste commit av filer och mappar label_parent_revision: Förälder label_child_revision: Barn @@ -904,7 +903,7 @@ sv: label_required: Nödvändig label_attribute_of_project: Projektets %{name} label_attribute_of_issue: Ärendets %{name} - label_attribute_of_author: Författarens %{name} + label_attribute_of_author: Skaparens %{name} label_attribute_of_assigned_to: Tilldelad användares %{name} label_attribute_of_user: Användarens %{name} label_attribute_of_fixed_version: Målversionens %{name} @@ -923,7 +922,7 @@ sv: button_expand_all: Expandera alla button_delete: Ta bort button_create: Skapa - button_create_and_continue: Skapa och fortsätt + button_create_and_continue: Skapa och ny button_test: Testa button_edit: Ändra button_edit_associated_wikipage: "Ändra associerad Wikisida: %{page_title}" @@ -937,15 +936,15 @@ sv: button_list: Lista button_view: Visa button_move: Flytta - button_move_and_follow: Flytta och följ efter + button_move_and_follow: Flytta och följ button_back: Tillbaka button_cancel: Avbryt button_activate: Aktivera button_sort: Sortera - button_log_time: Logga tid + button_log_time: Stämpla tid button_rollback: Återställ till denna version button_watch: Bevaka - button_unwatch: Stoppa bevakning + button_unwatch: Ta bort bevakning button_reply: Svara button_archive: Arkivera button_unarchive: Ta bort från arkiv @@ -953,7 +952,7 @@ sv: button_rename: Byt namn button_change_password: Ändra lösenord button_copy: Kopiera - button_copy_and_follow: Kopiera och följ efter + button_copy_and_follow: Kopiera och följ button_annotate: Kommentera button_update: Uppdatera button_configure: Konfigurera @@ -980,12 +979,12 @@ sv: field_active: Aktiv - text_select_mail_notifications: Välj för vilka händelser mail ska skickas. - text_regexp_info: eg. ^[A-Z0-9]+$ + text_select_mail_notifications: Välj för vilka händelser e-post ska skickas. + text_regexp_info: t.ex. ^[A-Z0-9]+$ text_project_destroy_confirmation: Är du säker på att du vill ta bort detta projekt och all relaterad data? text_subprojects_destroy_warning: "Alla underprojekt: %{value} kommer också tas bort." text_workflow_edit: Välj en roll och en ärendetyp för att ändra arbetsflöde - text_are_you_sure: Är du säker ? + text_are_you_sure: Är du säker? text_journal_changed: "%{label} ändrad från %{old} till %{new}" text_journal_changed_no_detail: "%{label} uppdaterad" text_journal_set_to: "%{label} satt till %{value}" @@ -1005,8 +1004,8 @@ sv: text_issues_ref_in_commit_messages: Referera och fixa ärenden i commit-meddelanden text_issue_added: "Ärende %{id} har rapporterats (av %{author})." text_issue_updated: "Ärende %{id} har uppdaterats (av %{author})." - text_wiki_destroy_confirmation: Är du säker på att du vill ta bort denna wiki och allt dess innehåll ? - text_issue_category_destroy_question: "Några ärenden (%{count}) är tilldelade till denna kategori. Vad vill du göra ?" + text_wiki_destroy_confirmation: Är du säker på att du vill ta bort denna wiki och allt dess innehåll? + text_issue_category_destroy_question: "Några ärenden (%{count}) är tilldelade denna kategori. Vad vill du göra?" text_issue_category_destroy_assignments: Ta bort kategoritilldelningar text_issue_category_reassign_to: Återtilldela ärenden till denna kategori text_user_mail_option: "För omarkerade projekt kommer du bara bli underrättad om saker du bevakar eller är inblandad i (T.ex. ärenden du skapat eller tilldelats)." @@ -1014,25 +1013,25 @@ sv: text_load_default_configuration: Läs in standardkonfiguration text_status_changed_by_changeset: "Tilldelad i changeset %{value}." text_time_logged_by_changeset: "Tilldelad i changeset %{value}." - text_issues_destroy_confirmation: 'Är du säker på att du vill radera markerade ärende(n) ?' + text_issues_destroy_confirmation: 'Är du säker på att du vill radera markerade ärende(n)?' text_issues_destroy_descendants_confirmation: Detta kommer även ta bort %{count} underaktivitet(er). text_time_entries_destroy_confirmation: Är du säker på att du vill ta bort valda tidloggningar? text_select_project_modules: 'Välj vilka moduler som ska vara aktiva för projektet:' text_default_administrator_account_changed: Standardadministratörens konto ändrat text_file_repository_writable: Arkivet för bifogade filer är skrivbart - text_plugin_assets_writable: Arkivet för plug-ins är skrivbart + text_plugin_assets_writable: Arkivet för tillägg är skrivbart text_minimagick_available: MiniMagick tillgängligt (ej obligatoriskt) - text_destroy_time_entries_question: "%{hours} timmar har rapporterats på ärendena du är på väg att ta bort. Vad vill du göra ?" + text_destroy_time_entries_question: "%{hours} timmar har rapporterats på ärendena du är på väg att ta bort. Vad vill du göra?" text_destroy_time_entries: Ta bort rapporterade timmar text_assign_time_entries_to_project: Tilldela rapporterade timmar till projektet text_reassign_time_entries: 'Återtilldela rapporterade timmar till detta ärende:' text_user_wrote: "%{value} skrev:" text_user_wrote_in: "%{value} skrev (%{link}):" - text_enumeration_destroy_question: "%{count} objekt är tilldelade till detta värde." + text_enumeration_destroy_question: "%{count} objekt är tilldelade detta värde." text_enumeration_category_reassign_to: 'Återtilldela till detta värde:' - text_email_delivery_not_configured: "Mailfunktionen har inte konfigurerats, och notifieringar via mail kan därför inte skickas.\nKonfigurera din SMTP-server i config/configuration.yml och starta om applikationen för att aktivera dem." - text_repository_usernames_mapping: "Välj eller uppdatera den Redmine-användare som är mappad till varje användarnamn i versionarkivloggen.\nAnvändare med samma användarnamn eller mailadress i både Redmine och versionsarkivet mappas automatiskt." - text_diff_truncated: '... Denna diff har förminskats eftersom den överskrider den maximala storlek som kan visas.' + text_email_delivery_not_configured: "E-postfunktionen har inte konfigurerats, och aviseringar via e-post kan därför inte skickas.\nKonfigurera din SMTP-server i <strong>config/configuration.yml</strong> och starta om applikationen för att aktivera dem." + text_repository_usernames_mapping: "Välj eller uppdatera den Redmine-användare som är mappad till varje användarnamn i versionarkivloggen.\nAnvändare med samma användarnamn eller e-postadress i både Redmine och versionsarkivet mappas automatiskt." + text_diff_truncated: '... Denna diff har kortats ner eftersom den överskrider den maximala storlek som kan visas.' text_custom_field_possible_values_info: 'Ett värde per rad' text_wiki_page_destroy_question: "Denna sida har %{descendants} underliggande sidor. Vad vill du göra?" text_wiki_page_nullify_children: "Behåll undersidor som rotsidor" @@ -1047,13 +1046,13 @@ sv: text_mercurial_repository_note: Lokalt versionsarkiv (t.ex. /hgrepo, c:\hgrepo) text_scm_command: Kommando text_scm_command_version: Version - text_scm_config: Du kan konfigurera dina scm-kommando i config/configuration.yml. Vänligen starta om applikationen när ändringar gjorts. + text_scm_config: Du kan konfigurera dina scm-kommando i <strong>config/configuration.yml</strong>. Vänligen starta om applikationen när ändringar gjorts. text_scm_command_not_available: Scm-kommando är inte tillgängligt. Vänligen kontrollera inställningarna i administratörspanelen. text_issue_conflict_resolution_overwrite: "Använd mina ändringar i alla fall (tidigare anteckningar kommer behållas men några ändringar kan bli överskrivna)" text_issue_conflict_resolution_add_notes: "Lägg till mina anteckningar och kasta mina andra ändringar" text_issue_conflict_resolution_cancel: "Kasta alla mina ändringar och visa igen %{link}" text_account_destroy_confirmation: "Är du säker på att du vill fortsätta?\nDitt konto kommer tas bort permanent, utan möjlighet att återaktivera det." - text_session_expiration_settings: "Varning: ändring av dessa inställningar kan få alla nuvarande sessioner, inklusive din egen, att gå ut." + text_session_expiration_settings: "Varning: ändring av dessa inställningar kan få alla nuvarande sessioner, inklusive din egen, att avslutas." text_project_closed: Detta projekt är stängt och skrivskyddat. text_turning_multiple_off: "Om du inaktiverar möjligheten till flera värden kommer endast ett värde per objekt behållas." @@ -1091,14 +1090,14 @@ sv: description_message_content: Meddelandeinnehåll description_query_sort_criteria_attribute: Sorteringsattribut description_query_sort_criteria_direction: Sorteringsriktning - description_user_mail_notification: Mailnotifieringsinställningar - description_available_columns: Tillgängliga Kolumner - description_selected_columns: Valda Kolumner + description_user_mail_notification: Inställningar för e-postaviseringar + description_available_columns: Tillgängliga kolumner + description_selected_columns: Valda kolumner description_all_columns: Alla kolumner description_issue_category_reassign: Välj ärendekategori description_wiki_subpages_reassign: Välj ny föräldersida text_repository_identifier_info: 'Endast gemener (a-z), siffror, streck och understreck är tillåtna.<br />När identifieraren sparats kan den inte ändras.' - notice_account_not_activated_yet: Du har inte aktiverat ditt konto än. Om du vill få ett nytt aktiveringsbrev, <a href="%{url}"> klicka på denna länk </a>. + notice_account_not_activated_yet: Du har inte aktiverat ditt konto än. Om du vill få ett nytt aktiveringsbrev, <a href="%{url}">klicka på denna länk</a>. notice_account_locked: Ditt konto är låst. label_hidden: Dold label_visibility_private: endast för mig @@ -1110,21 +1109,21 @@ sv: text_convert_available: ImageMagick-konvertering tillgänglig (valbart) label_link: Länk label_only: endast - label_drop_down_list: droppmeny + label_drop_down_list: rullgardinsmeny label_checkboxes: kryssrutor label_link_values_to: Länka värden till URL setting_force_default_language_for_anonymous: Lås till förvalt språk för anonyma användare setting_force_default_language_for_loggedin: Lås till förvalt språk för inloggade användare label_custom_field_select_type: Välj den typ av objekt som det anpassade fältet skall användas för - label_issue_assigned_to_updated: Tilldelad har uppdaterats + label_issue_assigned_to_updated: Tilldelning har uppdaterats label_check_for_updates: Leta efter uppdateringar label_latest_compatible_version: Senaste kompatibla version label_unknown_plugin: Okänt tillägg label_radio_buttons: alternativknappar label_group_anonymous: Anonyma användare label_group_non_member: Icke-medlemsanvändare - label_add_projects: Addera projekt - field_default_status: Default-status + label_add_projects: Lägg till projekt + field_default_status: Standardstatus text_subversion_repository_note: 'Exempel: file:///, http://, https://, svn://, svn+[tunnelscheme]://' field_users_visibility: Användares synlighet label_users_visibility_all: Alla aktiva användare @@ -1138,17 +1137,16 @@ sv: label_search_attachments_only: Sök endast bilagor label_search_open_issues_only: Endast öppna ärenden field_address: Epost - setting_max_additional_emails: Max antal ytterligare epost-adresser - label_email_address_plural: Epost - label_email_address_add: Addera epostadress - label_enable_notifications: Aktivera underrättelser - label_disable_notifications: Avaktivera underrättelser + setting_max_additional_emails: Maximalt antal extra e-postadresser + label_email_address_plural: E-post + label_email_address_add: Addera e-postadress + label_enable_notifications: Aktivera aviseringar + label_disable_notifications: Avaktivera aviseringar setting_search_results_per_page: Sökresultat per sida label_blank_value: blank permission_copy_issues: Kopiera ärenden - error_password_expired: Ditt lösenord har gått ut eller administratören kräver att - du ändrar det. - field_time_entries_visibility: Synlighet för tidsloggar + error_password_expired: Ditt lösenord har gått ut eller administratören kräver att du ändrar det. + field_time_entries_visibility: Synlighet för tidsstämplingar setting_password_max_age: Kräv lösenordsändring efter label_parent_task_attributes: Attribut för föräldraaktiviteter label_parent_task_attributes_derived: Kalkylerad från underaktiviteter @@ -1163,8 +1161,7 @@ sv: notice_import_finished: "%{count} artiklar har importerats" notice_import_finished_with_errors: "%{count} av %{total} artiklar kunde inte importeras" error_invalid_file_encoding: Filen är inte en %{encoding}-kodad fil - error_invalid_csv_file_or_settings: Filen är inte en CSV-fil eller stämmer inte med - inställningarna nedan (%{value}) + error_invalid_csv_file_or_settings: Filen är inte en CSV-fil eller stämmer inte med inställningarna nedan (%{value}) error_can_not_read_import_file: Fel vid läsning av fil att importera permission_import_issues: Importera ärenden label_import_issues: Importera ärenden @@ -1173,26 +1170,26 @@ sv: label_fields_wrapper: Fältomslag label_encoding: Kodning label_comma_char: Komma - label_semi_colon_char: Semicolon - label_quote_char: Citationstecken - label_double_quote_char: Dubbla citationstecken - label_fields_mapping: Kartläggning av fält + label_semi_colon_char: Semikolon + label_quote_char: Citattecken + label_double_quote_char: Dubbla citattecken + label_fields_mapping: Mappning av fält label_file_content_preview: Förhandsvisning av filinnehåll label_create_missing_values: Skapa saknade värden button_import: Importera - field_total_estimated_hours: Totalt uppskattad tid + field_total_estimated_hours: Totalt beräknad tid label_api: API label_total_plural: Totaler label_assigned_issues: Tilldelade ärenden label_field_format_enumeration: Nyckel/värde-lista label_f_hour_short: '%{value} h' - field_default_version: Defaultversion - error_attachment_extension_not_allowed: Otillåten utökning av bilaga %{extension} - setting_attachment_extensions_allowed: Tillåtna utökningar - setting_attachment_extensions_denied: Otillåtna utökningar + field_default_version: Standardversion + error_attachment_extension_not_allowed: Otillåten filtyp av bilaga %{extension} + setting_attachment_extensions_allowed: Tillåtna filtyper + setting_attachment_extensions_denied: Otillåtna filtyper label_any_open_issues: några öppna ärenden label_no_open_issues: inga öppna ärenden - label_default_values_for_new_users: Defaultvärden för nya användare + label_default_values_for_new_users: Standardvärden för nya användare error_ldap_bind_credentials: Ogiltigt LDAP konto/lösenord setting_sys_api_key: API-nyckel setting_lost_password: Glömt lösenord @@ -1201,25 +1198,21 @@ sv: mail_body_security_notification_change_to: ! '%{field} ändrades till %{value}.' mail_body_security_notification_add: ! '%{field} %{value} las till.' mail_body_security_notification_remove: ! '%{field} %{value} togs bort.' - mail_body_security_notification_notify_enabled: Epostadress %{value} kommer att få - underrättelser. - mail_body_security_notification_notify_disabled: Epostadress %{value} får inga fler - underrättelser. + mail_body_security_notification_notify_enabled: E-postadress %{value} kommer att få aviseringar. + mail_body_security_notification_notify_disabled: E-postadress %{value} får inga fler aviseringar. mail_body_settings_updated: ! 'Följande inställningar ändrades:' field_remote_ip: IP-adress label_wiki_page_new: Ny wiki-sida label_relations: Relationer - button_filter: Filter + button_filter: Filtrering mail_body_password_updated: Ditt lösenord är ändrat. label_no_preview: Ingen förhandsvisning tillgänglig - error_no_tracker_allowed_for_new_issue_in_project: Projektet har ingen spårare - som du kan skapa ett ärende för - label_tracker_all: All spårare + error_no_tracker_allowed_for_new_issue_in_project: Projektet har ingen ärendetyp som du kan skapa ett ärende för + label_tracker_all: All ärendetyper label_new_project_issue_tab_enabled: Visa fliken "Nya ärenden" setting_new_item_menu_tab: Projektmenyflik för att skapa nya objekt - label_new_object_tab_enabled: Visa "+" droppmeny - error_no_projects_with_tracker_allowed_for_new_issue: Inga projekt med spårare - tillåtna för nya ärenden + label_new_object_tab_enabled: Visa "+" rullgardinsmenyn + error_no_projects_with_tracker_allowed_for_new_issue: Inga projekt med ärendetyper tillåtna för nya ärenden field_textarea_font: Font som används för textytor label_font_default: Default font label_font_monospace: Monospaced font @@ -1227,94 +1220,81 @@ sv: setting_timespan_format: Format för tidsintervall label_table_of_contents: Innehållsförteckning setting_commit_logs_formatting: Använd textformatering för meddelanden - setting_mail_handler_enable_regex: Aktivera reguljära uttryck + setting_mail_handler_enable_regex: Aktivera reguljära uttryck (RegEx) error_move_of_child_not_possible: 'Underaktivitet %{child} kunde inte flyttas till det nya projektet: %{errors}' - error_cannot_reassign_time_entries_to_an_issue_about_to_be_deleted: Spenderad tid kan inte - flyttas till ett ärende som är på väg att tas bort - setting_timelog_required_fields: Nödvändiga fält för tidsangivelser - label_attribute_of_object: '%{object_name}''s %{name}' + error_cannot_reassign_time_entries_to_an_issue_about_to_be_deleted: Spenderad tid kan inte flyttas till ett ärende som är på väg att tas bort + setting_timelog_required_fields: Nödvändiga fält för tidsstämplingar + label_attribute_of_object: '%{object_name}s %{name}' label_user_mail_option_only_assigned: Bara för saker som jag följer eller är tilldelad label_user_mail_option_only_owner: Bara för saker som jag följer eller äger - warning_fields_cleared_on_bulk_edit: Ändringar kommer att orsaka automatiskt borttagande - av värden från ett eller flera fält för de valda objekten + warning_fields_cleared_on_bulk_edit: Ändringar kommer att orsaka automatiskt borttagande av värden från ett eller flera fält för de valda objekten field_updated_by: Uppdaterad av field_last_updated_by: Senast uppdaterad av field_full_width_layout: Layout med full bredd label_last_notes: Senaste noteringar field_digest: Checksumma - field_default_assigned_to: Defaultmottagare + field_default_assigned_to: Standardmottagare setting_show_custom_fields_on_registration: Visa anpassade fält vid registrering permission_view_news: Visa nyheter label_no_preview_alternative_html: Ingen förhandsvisning tillgänglig. %{link} filen istället. - label_no_preview_download: Nerladdning + label_no_preview_download: Nedladdning setting_close_duplicate_issues: Stäng duplicerade ärenden automatiskt - error_exceeds_maximum_hours_per_day: Kan inte logga mer än %{max_hours} timmar - samma dag (%{logged_hours} timmar har redan loggats) - setting_time_entry_list_defaults: Defaults för tidsloggning - setting_timelog_accept_0_hours: Acceptera tidsloggar med 0 timmar - setting_timelog_max_hours_per_day: Max timmar som kan loggas per dag och användare + error_exceeds_maximum_hours_per_day: Kan inte stämpla mer än %{max_hours} timmar samma dag (%{logged_hours} timmar har redan stämplats) + setting_time_entry_list_defaults: Standard för tidsstämplingar + setting_timelog_accept_0_hours: Acceptera tidsstämplingar på 0 timmar + setting_timelog_max_hours_per_day: Max timmar som kan stämplas per dag och användare label_x_revisions: "%{count} revisioner" - error_can_not_delete_auth_source: Denna autenticeringsmetod är i bruk och kan inte - tas bort. + error_can_not_delete_auth_source: Denna autentiseringsmetod är i bruk och kan inte tas bort. button_actions: Handlingar - mail_body_lost_password_validity: OBS att du endast kan ändra lösenordet en gång - via denna länk. - text_login_required_html: När autenticering inte krävs, är publika projekt och deras - innehåll tillgängliga på nätverket. Du kan <a href="%{anonymous_role_path}"> ändra - tillämpliga behörigheter</a>. + mail_body_lost_password_validity: Observera att du endast kan ändra lösenordet en gång via denna länk. + text_login_required_html: 'När autentisering inte krävs, är publika projekt och deras innehåll tillgängliga på nätverket. Du kan <a href="%{anonymous_role_path}"> ändra tillämpliga behörigheter</a>.' label_login_required_yes: 'Ja' label_login_required_no: Nej, tillåt anonym access till publika projekt - text_project_is_public_non_member: Publika projekt och deras innehåll är tillgängliga - för alla inloggade användare. - text_project_is_public_anonymous: Publika projekt och deras innehåll är tillgängliga - på nätverket. + text_project_is_public_non_member: Publika projekt och deras innehåll är tillgängliga för alla inloggade användare. + text_project_is_public_anonymous: Publika projekt och deras innehåll är tillgängliga på nätverket. label_version_and_files: Versioner (%{count}) och filer label_ldap: LDAP label_ldaps_verify_none: LDAPS (utan certifikatkontroll) label_ldaps_verify_peer: LDAPS - label_ldaps_warning: Det rekommenderas att använda krypterad LDAPS-förbindelse med - certifikatkontroll för att hindra manipulation av autenticeringsprocessen. + label_ldaps_warning: Det rekommenderas att använda krypterad LDAPS-koppling med certifikatkontroll för att hindra manipulation av autentiseringsprocessen. label_nothing_to_preview: Ingenting att visa error_token_expired: Lösenordslänken gäller inte längre, var god försök igen. - error_spent_on_future_date: Kan inte logga tid på framtida datum - setting_timelog_accept_future_dates: Acceptera tidsloggningar på framtida datum + error_spent_on_future_date: Kan inte stämpla tid på framtida datum + setting_timelog_accept_future_dates: Acceptera tidsstämplingar på framtida datum label_delete_link_to_subtask: Ta bort relation - error_not_allowed_to_log_time_for_other_users: Du får inte logga tid - för andra användare - permission_log_time_for_other_users: Logga spenderad tid för andra användare + error_not_allowed_to_log_time_for_other_users: Du får inte stämpla tid för andra användare + permission_log_time_for_other_users: Stämpla spenderad tid för andra användare label_tomorrow: imorgon label_next_week: nästa vecka label_next_month: nästa månad text_role_no_workflow: Inget arbetsflöde definierat för denna roll - text_status_no_workflow: Ingen spårare använder denna status i arbetsflödet - setting_mail_handler_preferred_body_part: Föredragen del av epost i flera delar(HTML) - setting_show_status_changes_in_mail_subject: Visa statusändringar i ärendemailunderrättelsers - ämne + text_status_no_workflow: Ingen arbetstyp använder denna status i arbetsflödet + setting_mail_handler_preferred_body_part: Föredragen del av epost i flera delar (HTML) + setting_show_status_changes_in_mail_subject: Visa statusändringar för ärenden i ämnet för e-postaviseringar label_inherited_from_parent_project: Ärvd från föräldraprojekt label_inherited_from_group: Ärvd från grupp%{name} - label_trackers_description: Beskrivning för spårare - label_open_trackers_description: Visa beskrivning för alla spårare + label_trackers_description: Beskrivning för ärendetyp + label_open_trackers_description: Visa beskrivning för alla ärendetyper label_preferred_body_part_text: Text label_preferred_body_part_html: HTML field_parent_issue_subject: Ämne för föräldraärende permission_edit_own_issues: Ändra egna ärenden - text_select_apply_tracker: Välj spårare - label_updated_issues: Updaterade ärenden - text_avatar_server_config_html: Nuvarande avatar-server <a href="%{url}">%{url}</a>. - Du kan konfigurera det i config/configuration.yml. - setting_gantt_months_limit: Max antal månader på gantt-schemat - permission_import_time_entries: Importera tidsangivelser - label_import_notifications: Skicka underrättelser med epost under importen - text_gs_available: ImageMagick PDF support tillgänglig (frivillig) + text_select_apply_tracker: Välj ärendetyp + label_updated_issues: Uppdaterade ärenden + text_avatar_server_config_html: 'Nuvarande avatar-server <a href="%{url}">%{url}</a>. Du kan konfigurera det i <strong>config/configuration.yml</strong>.' + setting_gantt_months_limit: Max antal månader på Gantt-schemat + permission_import_time_entries: Importera tidsstämplingar + label_import_notifications: Skicka aviseringar med e-post under importen + text_gs_available: ImageMagick PDF-support tillgänglig (frivillig) field_recently_used_projects: Antal nyligen använda projekt label_optgroup_bookmarks: Bokmärken label_optgroup_recents: Nyligen använda button_project_bookmark: Lägg till bokmärke button_project_bookmark_delete: Ta bort bokmärke - field_history_default_tab: Ärendes defaultflik för historik + field_history_default_tab: Ärendens standardflik för historik label_issue_history_properties: Ändrade egenskaper - label_issue_history_notes: Noter + label_issue_history_notes: Anteckningar label_last_tab_visited: Senast besökta flik field_unique_id: Unikt ID text_no_subject: inget ämne @@ -1324,214 +1304,165 @@ sv: label_password_char_class_digits: siffror label_password_char_class_special_chars: specialtecken text_characters_must_contain: Måste innehålla %{character_classes}. - label_starts_with: startar med - label_ends_with: slutar med + label_starts_with: börjar på + label_ends_with: slutar på label_issue_fixed_version_updated: Målversion uppdaterad - setting_project_list_defaults: Projektlista defaults + setting_project_list_defaults: Projektlista standardinställningar label_display_type: Visa resultat som label_display_type_list: Lista label_display_type_board: Anslagstavla label_my_bookmarks: Mina bokmärken - label_import_time_entries: Importera tidsangivelser - notice_issue_not_closable_by_open_tasks: This issue cannot be closed because it has - at least one open subtask. - notice_issue_not_closable_by_blocking_issue: This issue cannot be closed because it - is blocked by at least one open issue. - notice_issue_not_reopenable_by_closed_parent_issue: This issue cannot be reopened - because its parent issue is closed. - error_attachments_too_many: This file cannot be uploaded because it exceeds the maximum - number of files that can be attached simultaneously (%{max_number_of_files}) - error_bulk_download_size_too_big: These attachments cannot be bulk downloaded because - the total file size exceeds the maximum allowed size (%{max_size}) - error_can_not_execute_macro_html: Error executing the <strong>%{name}</strong> macro - (%{error}) - error_macro_does_not_accept_block: This macro does not accept a block of text - error_childpages_macro_no_argument: With no argument, this macro can be called from - wiki pages only - error_circular_inclusion: Circular inclusion detected - error_page_not_found: Page not found - error_filename_required: Filename required - error_invalid_size_parameter: Invalid size parameter - error_attachment_not_found: Attachment %{name} not found - field_passwd_changed_on: Password last changed - field_toolbar_language_options: Code highlighting toolbar languages - setting_bulk_download_max_size: Maximum total size for bulk download - setting_email_domains_allowed: Allowed email domains - setting_email_domains_denied: Disallowed email domains - setting_twofa: Two-factor authentication - permission_delete_project: Delete the project - label_optional: optional - label_user_mail_notify_about_high_priority_issues_html: Also notify me about issues - with a priority of <em>%{prio}</em> or higher - label_days_to_html: "%{days} days up to %{date}" - label_required_lower: required - label_download_all_attachments: Download all files - label_relations_mapping: Relations mapping - label_assign_to_me: Assign to me - button_disable: Disable - label_import_users: Import users - twofa__totp__name: Authenticator app - twofa__totp__text_pairing_info_html: Scan this QR code or enter the plain text key - into a TOTP app (e.g. <a href="https://support.google.com/accounts/answer/1066447">Google - Authenticator</a>, <a href="https://authy.com/download/">Authy</a>, <a href="https://guide.duo.com/third-party-accounts">Duo - Mobile</a>) and enter the code in the field below to activate two-factor authentication. - twofa__totp__label_plain_text_key: Plain text key - twofa__totp__label_activate: Enable authenticator app - twofa_currently_active: 'Currently active: %{twofa_scheme_name}' - twofa_not_active: Not activated - twofa_label_code: Code - twofa_hint_disabled_html: Setting <strong>%{label}</strong> will deactivate and unpair - two-factor authentication devices for all users. - twofa_hint_required_html: Setting <strong>%{label}</strong> will require all users - to set up two-factor authentication at their next login. - twofa_label_setup: Enable two-factor authentication - twofa_label_deactivation_confirmation: Disable two-factor authentication - twofa_notice_select: 'Please select the two-factor scheme you would like to use:' - twofa_warning_require: The administrator requires you to enable two-factor authentication. - twofa_activated: Two-factor authentication successfully enabled. It is recommended - to <a data-method="post" href="%{bc_path}">generate backup codes</a> for your account. - twofa_deactivated: Two-factor authentication disabled. - twofa_mail_body_security_notification_paired: Two-factor authentication successfully - enabled using %{field}. - twofa_mail_body_security_notification_unpaired: Two-factor authentication disabled - for your account. - twofa_mail_body_backup_codes_generated: New two-factor authentication backup codes - generated. - twofa_mail_body_backup_code_used: A two-factor authentication backup code has been - used. - twofa_invalid_code: Code is invalid or outdated. - twofa_label_enter_otp: Please enter your two-factor authentication code. - twofa_too_many_tries: Too many tries. - twofa_resend_code: Resend code - twofa_code_sent: An authentication code has been sent to you. - twofa_generate_backup_codes: Generate backup codes - twofa_text_generate_backup_codes_confirmation: This will invalidate all existing backup - codes and generate new ones. Would you like to continue? - twofa_notice_backup_codes_generated: Your backup codes have been generated. - twofa_warning_backup_codes_generated_invalidated: New backup codes have been generated. - Your existing codes from %{time} are now invalid. - twofa_label_backup_codes: Two-factor authentication backup codes - twofa_text_backup_codes_hint: Use these codes instead of a one-time password should - you not have access to your second factor. Each code can only be used once. It is - recommended to print and store them in a safe place. - twofa_text_backup_codes_created_at: Backup codes generated %{datetime}. - twofa_backup_codes_already_shown: Backup codes cannot be shown again, please <a data-method="post" - href="%{bc_path}">generate new backup codes</a> if required. - field_twofa_scheme: Two-factor authentication scheme - text_user_destroy_confirmation: Are you sure you want to delete this user and remove - all references to them? This cannot be undone. Often, locking a user instead of - deleting them is the better solution. To confirm, please enter their login (%{login}) - below. - text_project_destroy_enter_identifier: To confirm, please enter the project's identifier - (%{identifier}) below. - button_add_subtask: Add subtask - notice_invalid_watcher: 'Invalid watcher: User will not receive any notifications - because they do not have access to view this object.' - button_fetch_changesets: Fetch commits - permission_view_message_watchers: View message watchers list - permission_add_message_watchers: Add message watchers - permission_delete_message_watchers: Delete message watchers - label_message_watchers: Watchers - button_copy_link: Copy link - error_invalid_authenticity_token: Invalid form authenticity token. - error_query_statement_invalid: An error occurred while executing the query and has - been logged. Please report this error to your Redmine administrator. - permission_view_wiki_page_watchers: View wiki page watchers list - permission_add_wiki_page_watchers: Add wiki page watchers - permission_delete_wiki_page_watchers: Delete wiki page watchers - label_wiki_page_watchers: Watchers - label_attachment_description: File description - error_no_data_in_file: The file does not contain any data - field_twofa_required: Require two factor authentication - twofa_hint_optional_html: Setting <strong>%{label}</strong> will let users set up - two-factor authentication at will, unless it is required by one of their groups. - twofa_text_group_required: This setting is only effective when the global two factor - authentication setting is set to 'optional'. Currently, two factor authentication - is required for all users. - twofa_text_group_disabled: This setting is only effective when the global two factor - authentication setting is set to 'optional'. Currently, two factor authentication - is disabled. - field_default_issue_query: Default issue query + label_import_time_entries: Importera tidsstämplingar + notice_issue_not_closable_by_open_tasks: Det här ärendet kan inte stängas eftersom det har minst en öppen underaktivitet. + notice_issue_not_closable_by_blocking_issue: Det här ärendet kan inte stängas eftersom det blockeras av minst ett öppet ärende. + notice_issue_not_reopenable_by_closed_parent_issue: Det här ärendet kan inte återöppnas eftersom dess överordnade ärende är stängt. + error_attachments_too_many: 'Den här filen kan inte laddas upp eftersom den överskriderdet maximala antalet filer som kan bifogas samtidigt (%{max_number_of_files}).' + error_bulk_download_size_too_big: 'Dessa bilagor kan inte laddas ner samtidigt eftersom den totala filstorleken överskrider den maximalt tillåtna storleken (%{max_size}).' + error_can_not_execute_macro_html: 'Fel vid körning av makrot <strong>%{name}</strong> (%{error}).' + error_macro_does_not_accept_block: Det här makrot accepterar inte ett textblock + error_childpages_macro_no_argument: Utan argument kan det här makrot endast anropas från wikisidor + error_circular_inclusion: Cirkulär inbäddning upptäckt + error_page_not_found: Sidan kunde inte hittas + error_filename_required: Filnamn krävs + error_invalid_size_parameter: Ogiltig storleksparameter + error_attachment_not_found: Bilagan %{name} kunde inte hittas + field_passwd_changed_on: Lösenordet ändrades + field_toolbar_language_options: Språk för verktygsfält för kodmarkering + setting_bulk_download_max_size: Maximal totalstorlek för samtidig nedladdning + setting_email_domains_allowed: Tillåtna e-postdomäner + setting_email_domains_denied: Otillåtna e-postdomäner + setting_twofa: Tvåfaktorsautentisering + permission_delete_project: Ta bort projektet + label_optional: valfritt + label_user_mail_notify_about_high_priority_issues_html: 'Avisera mig också om ärenden med en prioritet på <em>%{prio}</em> eller högre' + label_days_to_html: "%{days} dagar fram till %{date}" + label_required_lower: obligatorisk + label_download_all_attachments: Ladda ner samtliga filer + label_relations_mapping: Relationsmappning + label_assign_to_me: Tilldela mig + button_disable: Inaktivera + label_import_users: Importera användare + twofa__totp__name: Autentiseringsapp + twofa__totp__text_pairing_info_html: 'Skanna den här QR-koden eller ange nyckeln i en autentiseringsapp (t.ex. <a href="https://support.google.com/accounts/answer/1066447">Google Authenticator</a>, <a href="https://authy.com/download/">Authy</a>, <a href="https://guide.duo.com/third-party-accounts"> Duo Mobile</a>) och ange sedan koden i fältet nedan för att aktivera tvåfaktorsautentisering.' + twofa__totp__label_plain_text_key: Nyckel i klartext + twofa__totp__label_activate: Aktivera autentiseringsapp + twofa_currently_active: 'För närvarande aktiv: %{twofa_scheme_name}' + twofa_not_active: Inte aktiverad + twofa_label_code: Kod + twofa_hint_disabled_html: Inställningen <strong>%{label}</strong> kommer att inaktivera och koppla bort tvåfaktorsautentiseringsenheter för alla användare. + twofa_hint_required_html: Inställningen <strong>%{label}</strong> kommer att kräva att alla användare konfigurerar tvåfaktorsautentisering vid sin nästa inloggning. + twofa_label_setup: Aktivera tvåfaktorsautentisering + twofa_label_deactivation_confirmation: Inaktivera tvåfaktorsautentisering + twofa_notice_select: 'Välj det tvåfaktorsalternativ du vill använda:' + twofa_warning_require: Administratören kräver att du aktiverar tvåfaktorsautentisering. + twofa_activated: 'Tvåfaktorsautentisering har aktiverats framgångsrikt. Det rekommenderas att du <a data-method="post" href="%{bc_path}">genererar reservkoder</a> för ditt konto.' + twofa_deactivated: Tvåfaktorsautentisering inaktiverad. + twofa_mail_body_security_notification_paired: 'Tvåfaktorsautentisering har framgångsrikt aktiverats med hjälp av %{field}.' + twofa_mail_body_security_notification_unpaired: Tvåfaktorsautentisering har inaktiverats för ditt konto. + twofa_mail_body_backup_codes_generated: Nya reservkoder för tvåfaktorsautentisering har genererats. + twofa_mail_body_backup_code_used: En reservkod för tvåfaktorsautentisering har använts. + twofa_invalid_code: Koden är ogiltig eller föråldrad. + twofa_label_enter_otp: Ange din kod för tvåfaktorsautentisering. + twofa_too_many_tries: För många försök. + twofa_resend_code: Skicka kod på nytt + twofa_code_sent: En autentiseringskod har skickats till dig. + twofa_generate_backup_codes: Generera reservkoder + twofa_text_generate_backup_codes_confirmation: Detta kommer att göra alla befintliga reservkoder obrukbara och generera nya. Vill du fortsätta? + twofa_notice_backup_codes_generated: Dina reservkoder har genererats + twofa_warning_backup_codes_generated_invalidated: 'Nya reservkoder har genererats. Dina befintliga koder från %{time} är nu obrukbara.' + twofa_label_backup_codes: Reservkoder för tvåfaktorsautentisering. + twofa_text_backup_codes_hint: Använd dessa koder istället för en engångslösenkod om du inte har tillgång till din tvåfaktorsautentiseringsapp. Varje kod kan endast användas en gång. Det rekommenderas att skriva ut dem och förvara dem på en säker plats. + twofa_text_backup_codes_created_at: Reservkoder genererades %{datetime}. + twofa_backup_codes_already_shown: 'Reservkoder kan inte visas igen, men du kan <a data-method="post" href="%{bc_path}">generera nya reservkoder</a> om så behövs.' + field_twofa_scheme: Tvåfaktorsautentiseringssystem + text_user_destroy_confirmation: 'Är du säker på att du vill ta bort den här användaren och ta bort alla referenser till denne? Detta kan inte ångras. Ofta är det bättre att låsa en användare istället för att ta bort den. För att bekräfta, vänligen ange deras inloggning (%{login}) nedan.' + text_project_destroy_enter_identifier: 'För att bekräfta, vänligen ange projektets identifierare (%{identifier}) nedan.' + button_add_subtask: Lägg till underaktivitet + notice_invalid_watcher: 'Ogiltig bevakare: Användaren kommer inte att ta emot några aviseringar eftersom de inte har åtkomst att visa detta objekt.' + button_fetch_changesets: Hämta commits + permission_view_message_watchers: Visa lista över meddelandebevakare + permission_add_message_watchers: Lägg till meddelandebevakare + permission_delete_message_watchers: Ta bort meddelandebevakare + label_message_watchers: Bevakare + button_copy_link: Kopiera länk + error_invalid_authenticity_token: Ogiltig autentiseringstoken för formuläret. + error_query_statement_invalid: Ett fel inträffade vid körning av frågan och har loggats. Vänligen rapportera detta fel till din Redmine-administratör. + permission_view_wiki_page_watchers: Visa lista över bevakare för wikisida + permission_add_wiki_page_watchers: Lägg till bevakare för wikisida + permission_delete_wiki_page_watchers: Ta bort bevakare för wikisida + label_wiki_page_watchers: Bevakare + label_attachment_description: Filbeskrivning + error_no_data_in_file: Filen innehåller inga data + field_twofa_required: Kräv tvåfaktorsautentisering + twofa_hint_optional_html: Inställningen <strong>%{label}</strong> kommer att låta användare konfigurera tvåfaktorsautentisering efter eget önskemål, om det inte krävs av någon av deras grupper. + twofa_text_group_required: Den här inställningen är endast giltig när den globala inställningen för tvåfaktorsautentisering är inställd på "valfritt". För närvarande krävs tvåfaktorsautentisering för alla användare. + twofa_text_group_disabled: Den här inställningen är endast giltig när den globala inställningen för tvåfaktorsautentisering är inställd på "valfritt". För närvarande är tvåfaktorsautentisering inaktiverad. + field_default_issue_query: Standardfiltrering för ärenden label_default_queries: - for_all_projects: For all projects - for_current_project: For current project - for_all_users: For all users - for_this_user: For this user - text_allowed_queries_to_select: Public (to any users) queries only selectable - text_all_migrations_have_been_run: All database migrations have been run - button_save_object: Save %{object_name} - button_edit_object: Edit %{object_name} - button_delete_object: Delete %{object_name} - text_setting_config_change: You can configure the behaviour in config/configuration.yml. - Please restart the application after editing it. - label_bulk_edit: Bulk edit - button_create_and_follow: Create and follow - label_subtask: Subtask - label_default_query: Default query - field_default_project_query: Default project query - label_required_administrators: required for administrators - twofa_hint_required_administrators_html: Setting <strong>%{label}</strong> behaves - like optional, but will require all users with administration rights to set up two-factor - authentication at their next login. - label_auto_watch_on: Auto watch - label_auto_watch_on_issue_contributed_to: Issues I contributed to - text_project_close_confirmation: Are you sure you want to close the '%{value}' project - to make it read-only? - text_project_reopen_confirmation: Are you sure you want to reopen the '%{value}' project? - text_project_archive_confirmation: Are you sure you want to archive the '%{value}' - project? - mail_destroy_project_failed: Project %{value} could not be deleted. - mail_destroy_project_successful: Project %{value} was deleted successfully. - mail_destroy_project_with_subprojects_successful: Project %{value} and its subprojects - were deleted successfully. - project_status_scheduled_for_deletion: scheduled for deletion - text_projects_bulk_destroy_confirmation: Are you sure you want to delete the selected - projects and related data? - text_projects_bulk_destroy_head: | - You are about to permanently delete the following projects, including possible subprojects and any related data. - Please review the information below and confirm that this is indeed what you want to do. - This action cannot be undone. - text_projects_bulk_destroy_confirm: To confirm, please enter "%{yes}" in the box below. - text_subprojects_bulk_destroy: 'including its subproject(s): %{value}' - field_current_password: Current password - sudo_mode_new_info_html: "<strong>What's happening?</strong> You need to reconfirm - your password before taking any administrative actions, this ensures your account - stays protected." - label_edited: Edited - label_time_by_author: "%{time} by %{author}" - field_default_time_entry_activity: Default spent time activity - field_is_member_of_group: Member of group - text_users_bulk_destroy_head: You are about to delete the following users and remove - all references to them. This cannot be undone. Often, locking users instead of deleting - them is the better solution. - text_users_bulk_destroy_confirm: To confirm, please enter "%{yes}" below. - permission_select_project_publicity: Set project public or private - label_auto_watch_on_issue_created: Issues I created - field_any_searchable: Any searchable text - label_contains_any_of: contains any of - button_apply_issues_filter: Apply issues filter - label_view_previous_annotation: View annotation prior to this change - label_has_been: has been - label_has_never_been: has never been - label_changed_from: changed from - label_issue_statuses_description: Issue statuses description - label_open_issue_statuses_description: View all issue statuses description - text_select_apply_issue_status: Select issue status - field_name_or_email_or_login: Name, email or login - text_default_active_job_queue_changed: Default queue adapter which is well suited - only for dev/test changed - label_option_auto_lang: auto - label_issue_attachment_added: Attachment added - field_estimated_remaining_hours: Estimated remaining time - field_last_activity_date: Last activity - setting_issue_done_ratio_interval: Done ratio options interval - setting_copy_attachments_on_issue_copy: Copy attachments on copy - field_thousands_delimiter: Thousands delimiter - label_involved_principals: Author / Previous assignee + for_all_projects: För alla projekt + for_current_project: För aktivt projekt + for_all_users: För alla användare + for_this_user: För den här användaren + text_allowed_queries_to_select: Endast publika (för alla användare) filtreringar kan väljas + text_all_migrations_have_been_run: Alla databas-migrationer har körts + button_save_object: 'Spara %{object_name}' + button_edit_object: 'Redigera %{object_name}' + button_delete_object: 'Ta bort %{object_name}' + text_setting_config_change: 'Du kan konfigurera beteendet i <strong>config/configuration.yml</strong>. Vänligen starta om applikationen efter att du har redigerat den.' + label_bulk_edit: Massredigera + button_create_and_follow: Skapa och öppna + label_subtask: Underaktivitet + label_default_query: Standardfiltrering + field_default_project_query: Standardfiltrering för projekt + label_required_administrators: obligatoriskt för administratörer + twofa_hint_required_administrators_html: 'Inställningen <strong>%{label}</strong> fungerar som valfri, men kommer att kräva att alla användare med administratörsrättigheter konfigurerar tvåfaktorsautentisering vid sin nästa inloggning.' + label_auto_watch_on: Automatisk bevakning + label_auto_watch_on_issue_contributed_to: Ärenden jag varit delaktig i + text_project_close_confirmation: 'Är du säker på att du vill stänga projektet "%{value}" för att göra det skrivskyddat?' + text_project_reopen_confirmation: 'Är du säker på att du vill återöppna projektet "%{value}"?' + text_project_archive_confirmation: 'Är du säker på att du vill arkivera projektet "%{value}"?' + mail_destroy_project_failed: 'Projektet %{value} kunde inte raderas.' + mail_destroy_project_successful: 'Projektet %{value} raderades framgångsrikt.' + mail_destroy_project_with_subprojects_successful: 'Projektet %{value} och dess underprojekt raderades framgångsrikt.' + project_status_scheduled_for_deletion: schemalagd för radering + text_projects_bulk_destroy_confirmation: Är du säker på att du vill radera de valda projekten och relaterad data? + text_projects_bulk_destroy_head: Du är på väg att permanent radera följande projekt, inklusive eventuella underprojekt och relaterad data. Vänligen granska informationen nedan och bekräfta att detta verkligen är vad du vill göra. Denna åtgärd kan inte ångras. + text_projects_bulk_destroy_confirm: 'För att bekräfta, vänligen ange "%{yes}" i rutan nedan.' + text_subprojects_bulk_destroy: 'inklusive dess underprojekt: %{value}' + field_current_password: Nuvarande lösenord + sudo_mode_new_info_html: '<strong>Vad händer?</strong> Du måste bekräfta ditt lösenord igen innan du vidtar några administrativa åtgärder, detta säkerställer att ditt konto förblir skyddat.' + label_edited: Redigerad + label_time_by_author: '%{time} av %{author}' + field_default_time_entry_activity: Standardaktivitet för spenderad tid + field_is_member_of_group: Medlem i grupp + text_users_bulk_destroy_head: Du är på väg att ta bort följande användare och ta bort alla referenser till dem. Detta kan inte ångras. Ofta är det bättre att låsa användare istället för att ta bort dem. + text_users_bulk_destroy_confirm: 'För att bekräfta, vänligen ange "%{yes}" nedan.' + permission_select_project_publicity: Ställ in projektet som publikt eller privat + label_auto_watch_on_issue_created: Ärenden jag har skapat + field_any_searchable: All sökbart text + label_contains_any_of: innehåller någon av + button_apply_issues_filter: Tillämpa filter för ärenden + label_view_previous_annotation: Visa kommentar före denna ändring + label_has_been: har varit + label_has_never_been: har aldrig varit + label_changed_from: ändrad från + label_issue_statuses_description: Beskrivning av ärendestatusar + label_open_issue_statuses_description: Visa alla beskrivningar av ärendestatusar + text_select_apply_issue_status: Välj ärendestatus + field_name_or_email_or_login: Namn, e-post eller inloggning + text_default_active_job_queue_changed: Standardköadapter som är lämpad endast för utveckling/test har ändrats + label_option_auto_lang: automatisk + label_issue_attachment_added: Bilaga tillagd + field_estimated_remaining_hours: Beräknad återstående tid + field_last_activity_date: Senaste aktivitet + setting_issue_done_ratio_interval: 'Alternativ för intervall för "% Klar"' + setting_copy_attachments_on_issue_copy: Kopiera bilagor vid kopiering + field_thousands_delimiter: Tusenavgränsare + label_involved_principals: 'Skapare / Tidigare tilldelad' label_attachment_summary: - zero: "%{filename}" - one: "%{filename} and 1 file" - other: "%{filename} and %{count} files" + zero: '%{filename}' + one: '%{filename} och 1 fil' + other: '%{filename} och %{count} filer' setting_wiki_tablesort_enabled: Javascript based table sorting in wiki content label_progressbar: Progress bar error_spent_on_closed_issue: Cannot log time on a closed issue @@ -1540,3 +1471,28 @@ sv: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/ta-IN.yml b/config/locales/ta-IN.yml index 6521837ec..c727a450d 100644 --- a/config/locales/ta-IN.yml +++ b/config/locales/ta-IN.yml @@ -1462,3 +1462,28 @@ ta-IN: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/th.yml b/config/locales/th.yml index b84a5d43a..e4eb61e5b 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -1503,3 +1503,28 @@ th: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/tr.yml b/config/locales/tr.yml index f1c8a8c0d..386f6297b 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1506,3 +1506,28 @@ tr: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 83868a72a..290c2edaa 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -1495,3 +1495,28 @@ uk: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/vi.yml b/config/locales/vi.yml index e1778a4bf..c639f66aa 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -1511,3 +1511,28 @@ vi: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 261970d0c..c23fb98f7 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -369,7 +369,7 @@ field_priority: 優先權 field_fixed_version: 版本 field_user: 用戶 - field_principal: User or Group + field_principal: 用戶或群組 field_role: 角色 field_homepage: 網站首頁 field_is_public: 公開 @@ -1521,3 +1521,28 @@ setting_display_related_issues_table_headers: 顯示表格標題欄位 error_can_not_remove_role_reason_members_html: "<p>下列專案中有成員 屬於此角色:<br>%{projects}</p>" + setting_reactions_enabled: 啟用表情回應功能 + reaction_text_x_other_users: + one: 1 位其他用戶 + other: "%{count} 位其他用戶" + text_setting_gravatar_default_initials_html: 用戶姓名的首字母縮寫將被送往 <a href="https://www.gravatar.com">https://www.gravatar.com</a> + 用以產生其大頭貼. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 11a5dcdb9..6008a0fdd 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1443,3 +1443,28 @@ zh: setting_display_related_issues_table_headers: Show table headers error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>" + setting_reactions_enabled: Enable reactions + reaction_text_x_other_users: + one: 1 other + other: "%{count} others" + text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> + to generate their avatars. + permission_view_project: View projects + permission_search_project: Search projects + permission_view_members: View project members + label_oauth_permission_admin: Administrate this Redmine + label_oauth_admin_access: Administration + label_oauth_application_plural: Applications + label_oauth_authorized_application_plural: Authorized applications + text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, + this application will be able to read and write all data and impersonate other users. + text_oauth_admin_permission_info: This application requests full administrative access. + If you are an Administrator (or become one in the future), it will be able to read + and write all data and impersonate other users on your behalf. If you want to avoid + this, authorize it as a user without Administrator privileges instead. + text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown + again. + text_oauth_implicit_permissions: View your name, login and primary email address + text_oauth_info_scopes: Select the scopes this application may request. The application + will not be allowed to do more than what is selected here. It will also always be + limited by the roles and project memberships of the user who authorized it. diff --git a/config/routes.rb b/config/routes.rb index 89927bee3..52c95c6a4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,6 +18,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Rails.application.routes.draw do + use_doorkeeper do + controllers :applications => 'oauth2_applications' + end + + root :to => 'welcome#index' root :to => 'welcome#index', :as => 'home' match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post] @@ -61,6 +66,8 @@ Rails.application.routes.draw do end end + resources :reactions, only: [:create, :destroy] + get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt' get '/issues/gantt', :to => 'gantts#show' diff --git a/config/settings.yml b/config/settings.yml index a0c256cdd..b1217fc0a 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -362,4 +362,6 @@ timelog_accept_closed_issues: show_status_changes_in_mail_subject: default: 1 wiki_tablesort_enabled: + default: 0 +reactions_enabled: default: 1 diff --git a/db/migrate/017_create_settings.rb b/db/migrate/017_create_settings.rb index 5768ca24c..f86a1c2c3 100644 --- a/db/migrate/017_create_settings.rb +++ b/db/migrate/017_create_settings.rb @@ -8,6 +8,7 @@ class CreateSettings < ActiveRecord::Migration[4.2] # Persist default settings for new installations Setting.create!(name: 'default_notification_option', value: Setting.default_notification_option) Setting.create!(name: 'text_formatting', value: Setting.text_formatting) + Setting.create!(name: 'wiki_tablesort_enabled', value: Setting.wiki_tablesort_enabled) end def self.down diff --git a/db/migrate/20250423065135_create_reactions.rb b/db/migrate/20250423065135_create_reactions.rb new file mode 100644 index 000000000..56f345e1b --- /dev/null +++ b/db/migrate/20250423065135_create_reactions.rb @@ -0,0 +1,11 @@ +class CreateReactions < ActiveRecord::Migration[7.2] + def change + create_table :reactions do |t| + t.references :reactable, polymorphic: true, null: false + t.references :user, null: false + t.timestamps null: false + end + add_index :reactions, [:reactable_type, :reactable_id, :user_id], unique: true + add_index :reactions, [:reactable_type, :reactable_id, :id] + end +end diff --git a/db/migrate/20250530185658_ensure_wiki_tablesort_setting_is_stored_in_db.rb b/db/migrate/20250530185658_ensure_wiki_tablesort_setting_is_stored_in_db.rb new file mode 100644 index 000000000..b64a42dbb --- /dev/null +++ b/db/migrate/20250530185658_ensure_wiki_tablesort_setting_is_stored_in_db.rb @@ -0,0 +1,8 @@ +class EnsureWikiTablesortSettingIsStoredInDb < ActiveRecord::Migration[7.2] + def change + unless Setting.where(name: "wiki_tablesort_enabled").exists? + setting = Setting.new(:name => "wiki_tablesort_enabled", :value => 1) + setting.save! + end + end +end diff --git a/db/migrate/20250611092155_create_doorkeeper_tables.rb b/db/migrate/20250611092155_create_doorkeeper_tables.rb new file mode 100644 index 000000000..9e8096be3 --- /dev/null +++ b/db/migrate/20250611092155_create_doorkeeper_tables.rb @@ -0,0 +1,68 @@ +class CreateDoorkeeperTables < ActiveRecord::Migration[7.2] + def change + create_table :oauth_applications do |t| + t.string :name, null: false + t.string :uid, null: false + t.string :secret, null: false + t.text :redirect_uri, null: false + t.text :scopes, null: false + t.boolean :confidential, null: false, default: true + t.timestamps null: false + end + + add_index :oauth_applications, :uid, unique: true + + create_table :oauth_access_grants do |t| + t.integer :resource_owner_id, null: false + t.references :application, null: false + t.string :token, null: false + t.integer :expires_in, null: false + t.text :redirect_uri, null: false + t.datetime :created_at, null: false + t.datetime :revoked_at + t.text :scopes + end + + add_index :oauth_access_grants, :token, unique: true + add_foreign_key( + :oauth_access_grants, + :oauth_applications, + column: :application_id + ) + add_foreign_key( + :oauth_access_grants, + :users, + column: :resource_owner_id + ) + + create_table :oauth_access_tokens do |t| + t.integer :resource_owner_id + t.references :application + + t.string :token, null: false + + t.string :refresh_token + t.integer :expires_in + t.datetime :revoked_at + t.datetime :created_at, null: false + t.text :scopes + + t.string :previous_refresh_token, null: false, default: "" + end + + add_index :oauth_access_tokens, :token, unique: true + add_index :oauth_access_tokens, :resource_owner_id + add_index :oauth_access_tokens, :refresh_token, unique: true + + add_foreign_key( + :oauth_access_tokens, + :oauth_applications, + column: :application_id + ) + add_foreign_key( + :oauth_access_tokens, + :users, + column: :resource_owner_id + ) + end +end diff --git a/db/migrate/20250611092227_enable_pkce.rb b/db/migrate/20250611092227_enable_pkce.rb new file mode 100644 index 000000000..cfc666f50 --- /dev/null +++ b/db/migrate/20250611092227_enable_pkce.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class EnablePkce < ActiveRecord::Migration[7.2] + def change + add_column :oauth_access_grants, :code_challenge, :string, null: true + add_column :oauth_access_grants, :code_challenge_method, :string, null: true + end +end diff --git a/doc/INSTALL b/doc/INSTALL index 54d51740b..f229fa509 100644 --- a/doc/INSTALL +++ b/doc/INSTALL @@ -136,10 +136,10 @@ Please do not enter your SMTP settings in environment.rb. == References -* https://www.redmine.org/wiki/redmine/RedmineInstall -* https://www.redmine.org/wiki/redmine/EmailConfiguration -* https://www.redmine.org/wiki/redmine/RedmineSettings -* https://www.redmine.org/wiki/redmine/RedmineRepositories -* https://www.redmine.org/wiki/redmine/RedmineReceivingEmails -* https://www.redmine.org/wiki/redmine/RedmineReminderEmails -* https://www.redmine.org/wiki/redmine/RedmineLDAP +* https://www.redmine.org/projects/redmine/wiki/RedmineInstall +* https://www.redmine.org/projects/redmine/wiki/EmailConfiguration +* https://www.redmine.org/projects/redmine/wiki/RedmineSettings +* https://www.redmine.org/projects/redmine/wiki/RedmineRepositories +* https://www.redmine.org/projects/redmine/wiki/RedmineReceivingEmails +* https://www.redmine.org/projects/redmine/wiki/RedmineReminderEmails +* https://www.redmine.org/projects/redmine/wiki/RedmineLDAP diff --git a/doc/UPGRADING b/doc/UPGRADING index 091b86283..b990ddba2 100644 --- a/doc/UPGRADING +++ b/doc/UPGRADING @@ -62,7 +62,7 @@ https://www.redmine.org/ directory for web server delivery. By default, Redmine automatically recompiles assets in production mode when the application starts. - This behavior can be controlled using the "config.assets.redmine_detect_update flag" from configuration file. + This behavior can be controlled using the "config.assets.redmine_detect_update" flag from configuration file. To manually compile assets or if automatic compilation is disabled: @@ -101,4 +101,4 @@ https://www.redmine.org/ == References -* https://www.redmine.org/wiki/redmine/RedmineUpgrade +* https://www.redmine.org/projects/redmine/wiki/RedmineUpgrade diff --git a/lib/plugins/gravatar/lib/gravatar.rb b/lib/plugins/gravatar/lib/gravatar.rb index 4dc27db52..316a01b19 100644 --- a/lib/plugins/gravatar/lib/gravatar.rb +++ b/lib/plugins/gravatar/lib/gravatar.rb @@ -69,7 +69,7 @@ module GravatarHelper options[:default] = CGI::escape(options[:default]) unless options[:default].nil? gravatar_api_url(email_hash).tap do |url| opts = [] - [:rating, :size, :default].each do |opt| + [:rating, :size, :default, :initials].each do |opt| unless options[opt].nil? value = h(options[opt]) opts << [opt, value].join('=') diff --git a/lib/redmine.rb b/lib/redmine.rb index 95b3b7f3f..78a1a6d8c 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -24,11 +24,6 @@ begin rescue LoadError # MiniMagick is not available end -begin - require 'commonmarker' unless Object.const_defined?(:Commonmarker) -rescue LoadError - # CommonMarker is not available -end module Redmine end diff --git a/lib/redmine/database.rb b/lib/redmine/database.rb index b3cbdc661..13c92b8a4 100644 --- a/lib/redmine/database.rb +++ b/lib/redmine/database.rb @@ -58,7 +58,7 @@ module Redmine # Returns true if the database is MySQL def mysql? - /mysql/i.match?(ActiveRecord::Base.connection.adapter_name) + /mysql|trilogy/i.match?(ActiveRecord::Base.connection.adapter_name) end def mysql_version diff --git a/lib/redmine/diff.rb b/lib/redmine/diff.rb index 40c444a42..c925d463a 100644 --- a/lib/redmine/diff.rb +++ b/lib/redmine/diff.rb @@ -76,13 +76,9 @@ module Redmine def line_to_html_raw(line, offsets) if offsets s = +'' - unless offsets.first == 0 - s << CGI.escapeHTML(line[0..offsets.first-1]) - end + s << CGI.escapeHTML(line[0..(offsets.first - 1)]) unless offsets.first == 0 s << '<span>' + CGI.escapeHTML(line[offsets.first..offsets.last]) + '</span>' - unless offsets.last == -1 - s << CGI.escapeHTML(line[offsets.last+1..-1]) - end + s << CGI.escapeHTML(line[(offsets.last + 1)..-1]) unless offsets.last == -1 s else CGI.escapeHTML(line) diff --git a/lib/redmine/field_format.rb b/lib/redmine/field_format.rb index 3e87eeb38..39b21c874 100644 --- a/lib/redmine/field_format.rb +++ b/lib/redmine/field_format.rb @@ -1145,11 +1145,11 @@ module Redmine end def formatted_value(view, custom_field, value, customized=nil, html=false) - text = "#{value}%" if html + text = "#{value}%" view.progress_bar(value.to_i, legend: (text if view.action_name == 'show')) else - text + value.to_s end end end diff --git a/lib/redmine/helpers/gantt.rb b/lib/redmine/helpers/gantt.rb index 5854a15f2..523ae3188 100644 --- a/lib/redmine/helpers/gantt.rb +++ b/lib/redmine/helpers/gantt.rb @@ -198,12 +198,18 @@ module Redmine # Returns the distinct versions of the issues that belong to +project+ def project_versions(project) - project_issues(project).filter_map(&:fixed_version).uniq + @project_versions ||= {} + @project_versions[project&.id] ||= begin + ids = project_issues(project).filter_map(&:fixed_version_id).uniq + Version.where(id: ids).to_a + end end # Returns the issues that belong to +project+ and are assigned to +version+ def version_issues(project, version) - project_issues(project).select {|issue| issue.fixed_version == version} + @version_issues ||= {} + @version_issues[[project&.id, version&.id]] ||= + project_issues(project).select {|issue| issue.fixed_version_id == version&.id} end def render(options={}) @@ -232,7 +238,7 @@ module Redmine render_object_row(project, options) increment_indent(options) do # render issue that are not assigned to a version - issues = project_issues(project).select {|i| i.fixed_version.nil?} + issues = project_issues(project).select {|i| i.fixed_version_id.nil?} render_issues(issues, options) # then render project versions and their issues versions = project_versions(project) @@ -748,7 +754,7 @@ module Redmine html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " " html_class << (version.overdue? ? 'version-overdue' : '') html_class << ' version-closed' unless version.open? - if version.start_date && version.due_date && version.visible_fixed_issues.completed_percent + if version.due_date && version.start_date && version.visible_fixed_issues.completed_percent progress_date = calc_progress_date(version.start_date, version.due_date, version.visible_fixed_issues.completed_percent) html_class << ' behind-start-date' if progress_date < self.date_from @@ -778,10 +784,14 @@ module Redmine tag_options[:id] = "issue-#{object.id}" tag_options[:class] = "issue-subject hascontextmenu" tag_options[:title] = object.subject - children = object.leaf? ? [] : object.children & project_issues(object.project) has_children = - children.present? && - children.collect(&:fixed_version).uniq.intersect?([object.fixed_version]) + if object.leaf? + false + else + children = object.children & project_issues(object.project) + fixed_version_id = object.fixed_version_id + children.any? {|child| child.fixed_version_id == fixed_version_id} + end when Version tag_options[:id] = "version-#{object.id}" tag_options[:class] = "version-name" diff --git a/lib/redmine/i18n.rb b/lib/redmine/i18n.rb index a79b4fc68..0b31cb235 100644 --- a/lib/redmine/i18n.rb +++ b/lib/redmine/i18n.rb @@ -173,24 +173,5 @@ module Redmine def current_language ::I18n.locale end - - # Custom backend based on I18n::Backend::Simple with the following changes: - # * available_locales are determined by looking at translation file names - class Backend < ::I18n::Backend::Simple - module Implementation - # Get available locales from the translations filenames - def available_locales - @available_locales ||= begin - redmine_locales = Dir[Rails.root / 'config' / 'locales' / '*.yml'].map { |f| File.basename(f, '.yml').to_sym } - super & redmine_locales - end - end - end - - # Adds custom pluralization rules - include ::I18n::Backend::Pluralization - # Adds fallback to default locale for untranslated strings - include ::I18n::Backend::Fallbacks - end end end diff --git a/lib/redmine/preparation.rb b/lib/redmine/preparation.rb index 822662e11..a7387f5dc 100644 --- a/lib/redmine/preparation.rb +++ b/lib/redmine/preparation.rb @@ -280,6 +280,11 @@ module Redmine {:controller => 'auth_sources', :action => 'index'}, :icon => 'server-authentication', :html => {:class => 'icon icon-server-authentication'} + menu.push :applications, {:controller => 'oauth2_applications', :action => 'index'}, + :if => Proc.new { Setting.rest_api_enabled? }, + :caption => :'doorkeeper.layouts.admin.nav.applications', + :icon => 'apps', + :html => {:class => 'icon icon-applications'} menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true, :icon => 'plugins', @@ -408,9 +413,7 @@ module Redmine WikiFormatting.map do |format| format.register :textile - if Object.const_defined?(:Commonmarker) - format.register :common_mark, label: 'CommonMark Markdown (GitHub Flavored)' - end + format.register :common_mark, label: 'CommonMark Markdown (GitHub Flavored)' end ActionView::Template.register_template_handler :rsb, Views::ApiTemplateHandler diff --git a/lib/redmine/quote_reply.rb b/lib/redmine/quote_reply.rb index 05737c079..2bf41d405 100644 --- a/lib/redmine/quote_reply.rb +++ b/lib/redmine/quote_reply.rb @@ -27,11 +27,11 @@ module Redmine def quote_reply(url, selector_for_content, icon_only: false) quote_reply_function = "quoteReply('#{j url}', '#{j selector_for_content}', '#{j Setting.text_formatting}')" - html_options = { class: 'icon icon-comment' } + html_options = { class: 'icon icon-quote' } html_options[:title] = l(:button_quote) if icon_only link_to_function( - sprite_icon('comment', l(:button_quote), icon_only: icon_only), + sprite_icon('quote-filled', l(:button_quote), icon_only: icon_only, style: :filled), quote_reply_function, html_options ) diff --git a/lib/redmine/reaction.rb b/lib/redmine/reaction.rb new file mode 100644 index 000000000..09fb78ef8 --- /dev/null +++ b/lib/redmine/reaction.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Reaction + # Types of objects that can have reactions + REACTABLE_TYPES = %w(Journal Issue Message News Comment) + + # Returns true if the user can view the reaction of the object + def self.visible?(object, user = User.current) + Setting.reactions_enabled? && object.visible?(user) + end + + # Returns true if the user can add/remove a reaction to/from the object + def self.editable?(object, user = User.current) + user.logged? && visible?(object, user) && object&.project&.active? + end + + module Reactable + extend ActiveSupport::Concern + + included do + has_many :reactions, as: :reactable, dependent: :delete_all + + attr_writer :reaction_detail + end + + class_methods do + # Preloads reaction details for a collection of objects + def preload_reaction_details(objects) + return unless Setting.reactions_enabled? + + details = ::Reaction.build_detail_map_for(objects, User.current) + + objects.each do |object| + object.reaction_detail = details.fetch(object.id) { ::Reaction::Detail.new } + end + end + end + + def reaction_detail + # Loads and returns reaction details if they are not already loaded. + # This is intended for cases where explicit preloading is unnecessary, + # such as retrieving reactions for a single issue on its detail page. + load_reaction_detail unless defined?(@reaction_detail) + @reaction_detail + end + + def load_reaction_detail + self.class.preload_reaction_details([self]) + end + end + end +end diff --git a/lib/redmine/wiki_formatting/common_mark/alerts_icons_filter.rb b/lib/redmine/wiki_formatting/common_mark/alerts_icons_filter.rb new file mode 100644 index 000000000..27429d778 --- /dev/null +++ b/lib/redmine/wiki_formatting/common_mark/alerts_icons_filter.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module WikiFormatting + module CommonMark + # Defines the mapping from alert type (from CSS class) to SVG icon name. + # These icon names must correspond to IDs in your SVG sprite sheet (e.g., icons.svg). + ALERT_TYPE_TO_ICON_NAME = { + 'note' => 'help', + 'tip' => 'bulb', + 'warning' => 'warning', + 'caution' => 'alert-circle', + 'important' => 'message-report', + }.freeze + + class AlertsIconsFilter < HTML::Pipeline::Filter + def call + doc.search("p.markdown-alert-title").each do |node| + parent_node = node.parent + parent_class_attr = parent_node['class'] # e.g., "markdown-alert markdown-alert-note" + next unless parent_class_attr + + # Extract the specific alert type (e.g., "note", "tip", "warning") + # from the parent div's classes. + match_data = parent_class_attr.match(/markdown-alert-(\w+)/) + next unless match_data && match_data[1] # Ensure a type is found + + alert_type = match_data[1] + + # Get the corresponding icon name from our map. + icon_name = ALERT_TYPE_TO_ICON_NAME[alert_type] + next unless icon_name # Skip if no specific icon is defined for this alert type + + icon_html = ApplicationController.helpers.sprite_icon(icon_name, node.text) + + if icon_html + # Replace the existing text node with the icon HTML and label (text). + node.children.first.replace(icon_html) + end + end + doc + end + end + end + end +end diff --git a/lib/redmine/wiki_formatting/common_mark/formatter.rb b/lib/redmine/wiki_formatting/common_mark/formatter.rb index eb765b6d6..8b7a18394 100644 --- a/lib/redmine/wiki_formatting/common_mark/formatter.rb +++ b/lib/redmine/wiki_formatting/common_mark/formatter.rb @@ -18,7 +18,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require 'html/pipeline' -require 'task_list/filter' module Redmine module WikiFormatting @@ -33,8 +32,9 @@ module Redmine autolink: true, footnotes: true, header_ids: nil, - tasklist: false, + tasklist: true, shortcodes: false, + alerts: true, }.freeze, # https://github.com/gjtorikian/commonmarker#parse-options @@ -46,6 +46,7 @@ module Redmine unsafe: true, github_pre_lang: false, hardbreaks: Redmine::Configuration['common_mark_enable_hardbreaks'] == true, + tasklist_classes: true, }.freeze, commonmarker_plugins: { syntax_highlighter: nil @@ -58,7 +59,7 @@ module Redmine SyntaxHighlightFilter, FixupAutoLinksFilter, ExternalLinksFilter, - TaskList::Filter + AlertsIconsFilter ], PIPELINE_CONFIG class Formatter diff --git a/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb b/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb index cdefc372b..af72adc32 100644 --- a/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb +++ b/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb @@ -68,6 +68,26 @@ module Redmine end } + # Allow class on div and p tags only for alert blocks + # (must be exactly: "markdown-alert markdown-alert-*" for div, and "markdown-alert-title" for p) + (allowlist[:attributes]["div"] ||= []) << "class" + (allowlist[:attributes]["p"] ||= []) << "class" + allowlist[:transformers].push lambda{|env| + node = env[:node] + return unless node.element? + + case node.name + when 'div' + unless /\Amarkdown-alert markdown-alert-[a-z]+\z/.match?(node['class']) + node.remove_attribute('class') + end + when 'p' + unless node['class'] == 'markdown-alert-title' + node.remove_attribute('class') + end + end + } + # Allow table cell alignment by style attribute # # Only necessary if we used the TABLE_PREFER_STYLE_ATTRIBUTES @@ -78,20 +98,58 @@ module Redmine # allowlist[:attributes]["td"] = %w(style) # allowlist[:css] = { properties: ["text-align"] } - # Allow `id` in a and li elements for footnotes - # and remove any `id` properties not matching for footnotes + # Allow `id` in a elements for footnotes allowlist[:attributes]["a"].push "id" - allowlist[:attributes]["li"] = %w(id) + # Remove any `id` property not matching for footnotes allowlist[:transformers].push lambda{|env| node = env[:node] - return unless node.name == "a" || node.name == "li" + return unless node.name == "a" return unless node.has_attribute?("id") - return if node.name == "a" && node["id"] =~ /\Afnref-\d+\z/ - return if node.name == "li" && node["id"] =~ /\Afn-\d+\z/ + return if node.name == "a" && node["id"] =~ /\Afnref(-\d+){1,2}\z/ node.remove_attribute("id") } + # allow `id` in li element for footnotes + # allow `class` in li element for task list items + allowlist[:attributes]["li"] = %w(id class) + allowlist[:transformers].push lambda{|env| + node = env[:node] + return unless node.name == "li" + + if node.has_attribute?("id") && !(node["id"] =~ /\Afn-\d+\z/) + node.remove_attribute("id") + end + + if node.has_attribute?("class") && node["class"] != "task-list-item" + node.remove_attribute("class") + end + } + + # allow input type = "checkbox" with class "task-list-item-checkbox" + # for task list items + allowlist[:elements].push('input') + allowlist[:attributes]["input"] = %w(class type) + allowlist[:transformers].push lambda{|env| + node = env[:node] + + return unless node.name == "input" + return if node['type'] == "checkbox" && node['class'] == "task-list-item-checkbox" + + node.replace(node.children) + } + + # allow class "contains-task-list" on ul for task list items + allowlist[:attributes]["ul"] = %w(class) + allowlist[:transformers].push lambda{|env| + node = env[:node] + + return unless node.name == "ul" + return if node["class"] == "contains-task-list" + + node.remove_attribute("class") + } + # https://github.com/rgrove/sanitize/issues/209 allowlist[:protocols].delete("a") allowlist[:transformers].push lambda{|env| diff --git a/lib/tasks/icons.rake b/lib/tasks/icons.rake index e50c450a1..269ef43e2 100644 --- a/lib/tasks/icons.rake +++ b/lib/tasks/icons.rake @@ -16,7 +16,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. unless Rails.env.production? - ICON_RELEASE_VERSION = "v3.19.0" + ICON_RELEASE_VERSION = "v3.33.0" ICON_DEFAULT_STYLE = "outline" SOURCE = URI.parse("https://raw.githubusercontent.com/tabler/tabler-icons/#{ICON_RELEASE_VERSION}/icons") diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index 0bb0de944..38d69e7c8 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -73,13 +73,13 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase # using default browser locale which depend on system locale for "real" browsers drivers def log_user(login, password) visit '/my/page' - assert_equal '/login', current_path + assert_current_path '/login', :ignore_query => true within('#login-form form') do fill_in 'username', :with => login fill_in 'password', :with => password find('input[name=login]').click end - assert_equal '/my/page', current_path + assert_current_path '/my/page', :ignore_query => true end def wait_for_ajax diff --git a/test/fixtures/reactions.yml b/test/fixtures/reactions.yml new file mode 100644 index 000000000..d8fcbfc1b --- /dev/null +++ b/test/fixtures/reactions.yml @@ -0,0 +1,51 @@ +--- +reaction_001: + id: 1 + reactable_type: Issue + reactable_id: 1 + user_id: 1 +reaction_002: + id: 2 + reactable_type: Issue + reactable_id: 1 + user_id: 2 +reaction_003: + id: 3 + reactable_type: Issue + reactable_id: 1 + user_id: 3 +reaction_004: + id: 4 + reactable_type: Journal + reactable_id: 1 + user_id: 2 +reaction_005: + id: 5 + reactable_type: Issue + reactable_id: 6 + user_id: 2 +reaction_006: + id: 6 + reactable_type: Journal + reactable_id: 4 + user_id: 2 +reaction_007: + id: 7 + reactable_type: News + reactable_id: 1 + user_id: 1 +reaction_008: + id: 8 + reactable_type: Comment + reactable_id: 1 + user_id: 2 +reaction_009: + id: 9 + reactable_type: Message + reactable_id: 7 + user_id: 2 +reaction_010: + id: 10 + reactable_type: News + reactable_id: 3 + user_id: 2 diff --git a/test/functional/attachments_controller_test.rb b/test/functional/attachments_controller_test.rb index 04fdb15d2..c2e7e2f7b 100644 --- a/test/functional/attachments_controller_test.rb +++ b/test/functional/attachments_controller_test.rb @@ -42,7 +42,7 @@ class AttachmentsControllerTest < Redmine::ControllerTest assert_response :success assert_equal 'text/html', @response.media_type - assert_select 'th.filename', :text => /issues_controller.rb\t\(révision 1484\)/ + assert_select 'th.filename', :text => /issues_controller\.rb \(révision 1484\)/ assert_select 'td.line-code', :text => /Demande créée avec succès/ end end @@ -61,7 +61,7 @@ class AttachmentsControllerTest < Redmine::ControllerTest assert_response :success assert_equal 'text/html', @response.media_type - assert_select 'th.filename', :text => /issues_controller.rb\t\(r\?vision 1484\)/ + assert_select 'th.filename', :text => /issues_controller\.rb \(r\?vision 1484\)/ assert_select 'td.line-code', :text => /Demande cr\?\?e avec succ\?s/ end end @@ -81,7 +81,7 @@ class AttachmentsControllerTest < Redmine::ControllerTest assert_response :success assert_equal 'text/html', @response.media_type - assert_select 'th.filename', :text => /issues_controller.rb\t\(révision 1484\)/ + assert_select 'th.filename', :text => /issues_controller\.rb \(révision 1484\)/ assert_select 'td.line-code', :text => /Demande créée avec succès/ end end diff --git a/test/functional/documents_controller_test.rb b/test/functional/documents_controller_test.rb index b59ecdc81..944f0b30f 100644 --- a/test/functional/documents_controller_test.rb +++ b/test/functional/documents_controller_test.rb @@ -113,9 +113,9 @@ class DocumentsControllerTest < Redmine::ControllerTest # adds a long description to the first document doc = documents(:documents_001) doc.update(:description => <<~LOREM) - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut egestas, mi vehicula varius varius, ipsum massa fermentum orci, eget tristique ante sem vel mi. Nulla facilisi. Donec enim libero, luctus ac sagittis sit amet, vehicula sagittis magna. Duis ultrices molestie ante, eget scelerisque sem iaculis vitae. Etiam fermentum mauris vitae metus pharetra condimentum fermentum est pretium. Proin sollicitudin elementum quam quis pharetra. Aenean facilisis nunc quis elit volutpat mollis. Aenean eleifend varius euismod. Ut dolor est, congue eget dapibus eget, elementum eu odio. Integer et lectus neque, nec scelerisque nisi. EndOfLineHere + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut egestas, mi vehicula varius varius, ipsum massa fermentum orci, eget tristique ante sem vel mi. Nulla facilisi. Donec enim libero, luctus ac sagittis sit amet, vehicula sagittis magna. Duis ultrices molestie ante, eget scelerisque sem iaculis vitae. Etiam fermentum mauris vitae metus pharetra condimentum fermentum est pretium. Proin sollicitudin elementum quam quis pharetra. Aenean facilisis nunc quis elit volutpat mollis. Aenean eleifend varius euismod. Ut dolor est, congue eget dapibus eget, elementum eu odio. Integer et lectus neque, nec scelerisque nisi. EndOfLineHere - Vestibulum non velit mi. Aliquam scelerisque libero ut nulla fringilla a sollicitudin magna rhoncus. Praesent a nunc lorem, ac porttitor eros. Sed ac diam nec neque interdum adipiscing quis quis justo. Donec arcu nunc, fringilla eu dictum at, venenatis ac sem. Vestibulum quis elit urna, ac mattis sapien. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Vestibulum non velit mi. Aliquam scelerisque libero ut nulla fringilla a sollicitudin magna rhoncus. Praesent a nunc lorem, ac porttitor eros. Sed ac diam nec neque interdum adipiscing quis quis justo. Donec arcu nunc, fringilla eu dictum at, venenatis ac sem. Vestibulum quis elit urna, ac mattis sapien. Lorem ipsum dolor sit amet, consectetur adipiscing elit. LOREM get(:index, :params => {:project_id => 'ecookbook'}) assert_response :success diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index b5180fcff..48304c868 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -1737,7 +1737,7 @@ class IssuesControllerTest < Redmine::ControllerTest assert_select 'td.last_notes[colspan="4"]', :text => 'Some notes with Redmine links: #2, r2.' assert_select( 'td.last_notes[colspan="4"]', - :text => 'A comment with inline image: and a reference to #1 and r2.' + :text => 'A comment with inline image: and a reference to #1 and r2.' ) get( :index, @@ -3331,6 +3331,42 @@ class IssuesControllerTest < Redmine::ControllerTest assert_select 'span.badge.badge-private', text: 'Private' end + def test_show_should_display_reactions + current_user = User.generate! + + User.add_to_project(current_user, projects(:projects_001), + Role.generate!(users_visibility: 'members_of_visible_projects', permissions: [:view_issues])) + + @request.session[:user_id] = current_user.id + + get :show, params: { id: 1 } + + assert_response :success + + assert_select 'span[data-reaction-button-id=reaction_issue_1]' do + # The current_user can only see members who belong to projects that the current_user has access to. + # Since the Redmine Admin user does not belong to any projects visible to the current_user, + # the Redmine Admin user's name is not displayed in the reaction user list. Instead, "1 other" is shown. + assert_select 'a.reaction-button[title=?]', 'Dave Lopper and John Smith' do + assert_select 'span.icon-label', '2' + end + end + + assert_select 'span[data-reaction-button-id=reaction_journal_1]' do + assert_select 'a.reaction-button[title=?]', 'John Smith' + end + assert_select 'span[data-reaction-button-id=reaction_journal_2] a.reaction-button' + end + + def test_should_not_display_reactions_when_reactions_feature_is_disabled + with_settings reactions_enabled: '0' do + get :show, params: { id: 1 } + + assert_response :success + assert_select 'span[data-reaction-button-id]', false + end + end + def test_show_should_not_display_edit_attachment_icon_for_user_without_edit_issue_permission_on_tracker role = Role.find(2) role.set_permission_trackers 'edit_issues', [2, 3] diff --git a/test/functional/messages_controller_test.rb b/test/functional/messages_controller_test.rb index 74b9a3070..997b2263a 100644 --- a/test/functional/messages_controller_test.rb +++ b/test/functional/messages_controller_test.rb @@ -123,6 +123,27 @@ class MessagesControllerTest < Redmine::ControllerTest assert_select 'h3', {text: /Watchers \(\d*\)/, count: 0} end + def test_show_should_display_reactions + @request.session[:user_id] = 2 + + get :show, params: { board_id: 1, id: 4 } + + assert_response :success + assert_select 'span[data-reaction-button-id=reaction_message_4] a.reaction-button' do + assert_select 'svg use[href*=thumb-up]' + end + assert_select 'span[data-reaction-button-id=reaction_message_5] a.reaction-button' + assert_select 'span[data-reaction-button-id=reaction_message_6] a.reaction-button' + + # Should not display reactions when reactions feature is disabled. + with_settings reactions_enabled: '0' do + get :show, params: { board_id: 1, id: 4 } + + assert_response :success + assert_select 'span[data-reaction-button-id]', false + end + end + def test_get_new @request.session[:user_id] = 2 get(:new, :params => {:board_id => 1}) diff --git a/test/functional/news_controller_test.rb b/test/functional/news_controller_test.rb index f1ddfff71..536814c9d 100644 --- a/test/functional/news_controller_test.rb +++ b/test/functional/news_controller_test.rb @@ -106,6 +106,23 @@ class NewsControllerTest < Redmine::ControllerTest assert_response :not_found end + def test_show_should_display_reactions + @request.session[:user_id] = 1 + + get :show, params: { id: 1 } + assert_response :success + assert_select 'span[data-reaction-button-id=reaction_news_1] a.reaction-button.reacted' + assert_select 'span[data-reaction-button-id=reaction_comment_1] a.reaction-button' + + # Should not display reactions when reactions feature is disabled. + with_settings reactions_enabled: '0' do + get :show, params: { id: 1 } + + assert_response :success + assert_select 'span[data-reaction-button-id]', false + end + end + def test_get_new_with_project_id @request.session[:user_id] = 2 get(:new, :params => {:project_id => 1}) diff --git a/test/functional/reactions_controller_test.rb b/test/functional/reactions_controller_test.rb new file mode 100644 index 000000000..b65794969 --- /dev/null +++ b/test/functional/reactions_controller_test.rb @@ -0,0 +1,394 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require_relative '../test_helper' + +class ReactionsControllerTest < Redmine::ControllerTest + setup do + Setting.reactions_enabled = '1' + # jsmith + @request.session[:user_id] = users(:users_002).id + end + + teardown do + Setting.clear_cache + end + + test 'create for issue' do + issue = issues(:issues_002) + + assert_difference( + ->{ Reaction.count } => 1, + ->{ issue.reactions.by(users(:users_002)).count } => 1 + ) do + post :create, params: { + object_type: 'Issue', + object_id: issue.id + }, xhr: true + end + + assert_response :success + end + + test 'create for journal' do + journal = journals(:journals_005) + + assert_difference( + ->{ Reaction.count } => 1, + ->{ journal.reactions.by(users(:users_002)).count } => 1 + ) do + post :create, params: { + object_type: 'Journal', + object_id: journal.id + }, xhr: true + end + + assert_response :success + end + + test 'create for news' do + news = news(:news_002) + + assert_difference( + ->{ Reaction.count } => 1, + ->{ news.reactions.by(users(:users_002)).count } => 1 + ) do + post :create, params: { + object_type: 'News', + object_id: news.id + }, xhr: true + end + + assert_response :success + end + + test 'create reaction for comment' do + comment = comments(:comments_002) + + assert_difference( + ->{ Reaction.count } => 1, + ->{ comment.reactions.by(users(:users_002)).count } => 1 + ) do + post :create, params: { + object_type: 'Comment', + object_id: comment.id + }, xhr: true + end + + assert_response :success + end + + test 'create for message' do + message = messages(:messages_001) + + assert_difference( + ->{ Reaction.count } => 1, + ->{ message.reactions.by(users(:users_002)).count } => 1 + ) do + post :create, params: { + object_type: 'Message', + object_id: message.id + }, xhr: true + end + + assert_response :success + end + + test 'destroy for issue' do + reaction = reactions(:reaction_005) + + assert_difference 'Reaction.count', -1 do + delete :destroy, params: { + id: reaction.id, + # Issue (id=6) + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :success + assert_not Reaction.exists?(reaction.id) + end + + test 'destroy for journal' do + reaction = reactions(:reaction_006) + + assert_difference 'Reaction.count', -1 do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :success + assert_not Reaction.exists?(reaction.id) + end + + test 'destroy for news' do + # For News(id=3) + reaction = reactions(:reaction_010) + + assert_difference 'Reaction.count', -1 do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :success + assert_not Reaction.exists?(reaction.id) + end + + test 'destroy for comment' do + # For Comment(id=1) + reaction = reactions(:reaction_008) + + assert_difference 'Reaction.count', -1 do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :success + assert_not Reaction.exists?(reaction.id) + end + + test 'destroy for message' do + reaction = reactions(:reaction_009) + + assert_difference 'Reaction.count', -1 do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :success + assert_not Reaction.exists?(reaction.id) + end + + test 'create should respond with 403 when feature is disabled' do + Setting.reactions_enabled = '0' + # admin + @request.session[:user_id] = users(:users_001).id + + assert_no_difference 'Reaction.count' do + post :create, params: { + object_type: 'Issue', + object_id: issues(:issues_002).id + }, xhr: true + end + assert_response :forbidden + end + + test 'destroy should respond with 403 when feature is disabled' do + Setting.reactions_enabled = '0' + # admin + @request.session[:user_id] = users(:users_001).id + + reaction = reactions(:reaction_001) + assert_no_difference 'Reaction.count' do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + assert_response :forbidden + end + + test 'create by anonymou user should respond with 401 when feature is disabled' do + Setting.reactions_enabled = '0' + @request.session[:user_id] = nil + + assert_no_difference 'Reaction.count' do + post :create, params: { + object_type: 'Issue', + object_id: issues(:issues_002).id + }, xhr: true + end + assert_response :unauthorized + end + + test 'create by anonymous user should respond with 401' do + @request.session[:user_id] = nil + + assert_no_difference 'Reaction.count' do + post :create, params: { + object_type: 'Issue', + # Issue(id=1) is an issue in a public project + object_id: issues(:issues_001).id + }, xhr: true + end + + assert_response :unauthorized + end + + test 'destroy by anonymous user should respond with 401' do + @request.session[:user_id] = nil + + reaction = reactions(:reaction_002) + assert_no_difference 'Reaction.count' do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :unauthorized + end + + test 'create when reaction already exists should not create a new reaction and succeed' do + assert_no_difference 'Reaction.count' do + post :create, params: { + object_type: 'Comment', + # user(jsmith) has already reacted to Comment(id=1) + object_id: comments(:comments_001).id + }, xhr: true + end + + assert_response :success + end + + test 'destroy another user reaction should not destroy the reaction and succeed' do + # admin user's reaction + reaction = reactions(:reaction_001) + + assert_no_difference 'Reaction.count' do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :success + end + + test 'destroy nonexistent reaction' do + # For Journal(id=4) + reaction = reactions(:reaction_006) + reaction.destroy! + + assert_not Reaction.exists?(reaction.id) + + assert_no_difference 'Reaction.count' do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :success + end + + test 'create with invalid object type should respond with 403' do + # admin + @request.session[:user_id] = users(:users_001).id + + post :create, params: { + object_type: 'InvalidType', + object_id: 1 + }, xhr: true + + assert_response :forbidden + end + + test 'create without permission to view should respond with 403' do + # dlopper + @request.session[:user_id] = users(:users_003).id + + assert_no_difference 'Reaction.count' do + post :create, params: { + object_type: 'Issue', + # dlopper is not a member of the project where the issue (id=4) belongs. + object_id: issues(:issues_004).id + }, xhr: true + end + + assert_response :forbidden + end + + test 'destroy without permission to view should respond with 403' do + # dlopper + @request.session[:user_id] = users(:users_003).id + + # For Issue(id=6) + reaction = reactions(:reaction_005) + + assert_no_difference 'Reaction.count' do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :forbidden + end + + test 'create should respond with 404 for non-JS requests' do + issue = issues(:issues_002) + + assert_no_difference 'Reaction.count' do + post :create, params: { + object_type: 'Issue', + object_id: issue.id + } # Sending an HTML request by omitting xhr: true + end + + assert_response :not_found + end + + test 'create should respond with 403 when project is closed' do + issue = issues(:issues_010) + issue.project.update!(status: Project::STATUS_CLOSED) + + assert_no_difference 'Reaction.count' do + post :create, params: { + object_type: 'Issue', + object_id: issue.id + }, xhr: true + end + + assert_response :forbidden + end + + test 'destroy should respond with 403 when project is closed' do + reaction = reactions(:reaction_005) + reaction.reactable.project.update!(status: Project::STATUS_CLOSED) + + assert_no_difference 'Reaction.count' do + delete :destroy, params: { + id: reaction.id, + object_type: reaction.reactable_type, + object_id: reaction.reactable_id + }, xhr: true + end + + assert_response :forbidden + end +end diff --git a/test/functional/workflows_controller_test.rb b/test/functional/workflows_controller_test.rb index b30559d80..dcdc8d5bb 100644 --- a/test/functional/workflows_controller_test.rb +++ b/test/functional/workflows_controller_test.rb @@ -211,6 +211,45 @@ class WorkflowsControllerTest < Redmine::ControllerTest assert w.assignee end + def test_post_edit_with_large_number_of_statuses + # This test ensures that workflows with many statuses can be saved. + # Without setting `ENV['RACK_QUERY_PARSER_PARAMS_LIMIT']`, this raises + # ActionController::BadRequest exception due to exceeding the default + # query parameter limit of 4096. + WorkflowTransition.delete_all + + num_statuses = 40 + transitions_data = {} + + # Allowed statuses for a new issue (status_id = 0) + transitions_data['0'] = {} + (1..num_statuses).each do |status_id| + transitions_data['0'][status_id.to_s] = {'always' => '1'} + end + + # Status transitions between statuses + (1..num_statuses).each do |status_id_from| # rubocop:disable RuboCopStyle/CombinableLoops + transitions_data[status_id_from.to_s] = {} + (1..num_statuses).each do |status_id_to| + # skip self-transitions + next if status_id_from == status_id_to + + transitions_data[status_id_from.to_s][status_id_to.to_s] = { + 'always' => '1', 'author' => '1', 'assignee' => '1' + } + end + end + + assert_nothing_raised do + patch :update, :params => { + :role_id => 2, + :tracker_id => 1, + :transitions => transitions_data + } + end + assert_response :found + end + def test_get_permissions get :permissions diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb index f959744e2..2e2e8b933 100644 --- a/test/helpers/application_helper_test.rb +++ b/test/helpers/application_helper_test.rb @@ -1732,6 +1732,46 @@ class ApplicationHelperTest < Redmine::HelperTest end end + def test_section_edit_links_with_multiline_heading + raw = <<~RAW + # Wiki + + ## `Foo` Bar + + The heading above generates multiline HTML. + Don't assume heading tags are always single-line. + + ``` + <h2> + <code>Foo</code> Bar</h2> + ``` + RAW + @project = Project.find(1) + set_language_if_valid 'en' + with_settings :text_formatting => 'common_mark' do + result = + textilizable( + raw, + :edit_section_links => + {:controller => 'wiki', :action => 'edit', + :project_id => '1', :id => 'Test'} + ).delete("\n") + + assert_match( + Regexp.new( + '<div class="contextual heading-2" title="Edit this section" id="section-2">' \ + '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\?section=2">' \ + '<svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/icons-.*\.svg#icon--edit"></use></svg>' \ + '<span class="icon-label">Edit this section</span>' \ + '</a></div>' \ + '<a name="Foo-Bar"></a>' \ + '<h2 ><code>Foo</code> Bar<a href="#Foo-Bar" class="wiki-anchor">¶</a></h2>' + ), + result + ) + end + end + def test_default_formatter with_settings :text_formatting => 'unknown' do text = 'a *link*: http://www.example.net/' @@ -2363,6 +2403,14 @@ class ApplicationHelperTest < Redmine::HelperTest assert_equal expected, format_activity_description(text) end + def test_render_flash_messages_should_ignore_non_string_values + flash[:array_value] = ['1', '2'] + flash[:hash_value] = { foo: 'bar' } + + result = render_flash_messages + assert_equal '', result + end + private def wiki_links_with_special_characters diff --git a/test/helpers/avatars_helper_test.rb b/test/helpers/avatars_helper_test.rb index f407ae09e..baa64a653 100644 --- a/test/helpers/avatars_helper_test.rb +++ b/test/helpers/avatars_helper_test.rb @@ -68,6 +68,18 @@ class AvatarsHelperTest < Redmine::HelperTest assert_include 'class="gravatar picture"', avatar('jsmith <jsmith@somenet.foo>', :class => 'picture') end + def test_avatar_with_initials + with_settings :gravatar_default => 'initials' do + assert_include 'initials="RA"', avatar(User.find(1)) + end + end + + def test_avatar_should_reject_initials_if_default_is_not_initials + with_settings :gravatar_default => 'identicon' do + assert_not_include 'initials="RA"', avatar(User.find(1)) + end + end + def test_avatar_disabled with_settings :gravatar_enabled => '0' do assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo')) diff --git a/test/helpers/icons_helper_test.rb b/test/helpers/icons_helper_test.rb index ab0b58db4..7ef071f86 100644 --- a/test/helpers/icons_helper_test.rb +++ b/test/helpers/icons_helper_test.rb @@ -71,6 +71,13 @@ class IconsHelperTest < Redmine::HelperTest assert_match expected, icon end + def test_sprite_icon_should_return_svg_with_filled_class_when_style_is_filled + expected = %r{<svg class="s18 icon-svg icon-svg-filled" aria-hidden="true"><use href="/assets/icons-\w+.svg#icon--edit"></use></svg>$} + icon = sprite_icon('edit', style: :filled) + + assert_match expected, icon + end + def test_file_icon_should_return_folder_icon_for_directory entry = stub(:is_dir? => true) expected = %r{<svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/icons-\w+.svg#icon--folder"></use></svg><span class="icon-label">folder_name</span>} diff --git a/test/helpers/journals_helper_test.rb b/test/helpers/journals_helper_test.rb index 355d5ec6f..47f7b7749 100644 --- a/test/helpers/journals_helper_test.rb +++ b/test/helpers/journals_helper_test.rb @@ -47,10 +47,27 @@ class JournalsHelperTest < Redmine::HelperTest journals = issue.visible_journals_with_index # add indice journal_actions = render_journal_actions(issue, journals.first, {reply_links: true}) - assert_select_in journal_actions, 'a[title=?][class="icon icon-comment"]', 'Quote' + assert_select_in journal_actions, 'a[title=?][class="icon icon-quote"]', 'Quote' assert_select_in journal_actions, 'a[title=?][class="icon-only icon-edit"]', 'Edit' assert_select_in journal_actions, 'div[class="drdn-items"] a[class="icon icon-del"]' assert_select_in journal_actions, 'div[class="drdn-items"] a[class="icon icon-copy-link"]' + assert_select_in journal_actions, 'span.reaction-button-wrapper' + end + + def test_render_journal_actions_with_journal_without_notes + User.current = User.find(1) + issue = Issue.find(1) + issue.journals.first.update!(notes: '') + + journals = issue.visible_journals_with_index + + journal_actions = render_journal_actions(issue, journals.first, reply_links: true) + + assert_select_in journal_actions, 'span.reaction-button-wrapper' + assert_select_in journal_actions, 'span.drdn' + + assert_select_in journal_actions, 'a[class="icon-comment"]', false + assert_select_in journal_actions, 'a[class="icon-edit"]', false end def test_journal_thumbnail_attachments_should_be_in_the_same_order_as_the_journal_details diff --git a/test/helpers/reactions_helper_test.rb b/test/helpers/reactions_helper_test.rb new file mode 100644 index 000000000..1c5c82418 --- /dev/null +++ b/test/helpers/reactions_helper_test.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require_relative '../test_helper' + +class ReactionsHelperTest < ActionView::TestCase + include ReactionsHelper + + setup do + User.current = users(:users_002) + Setting.reactions_enabled = '1' + end + + teardown do + Setting.clear_cache + end + + test 'reaction_id_for generates a DOM id' do + assert_equal "reaction_issue_1", reaction_id_for(issues(:issues_001)) + end + + test 'reaction_button returns nil when feature is disabled' do + Setting.reactions_enabled = '0' + + assert_nil reaction_button(issues(:issues_004)) + end + + test 'reaction_button returns nil when object not visible' do + User.current = users(:users_003) + + assert_nil reaction_button(issues(:issues_004)) + end + + test 'reaction_button for anonymous users shows readonly button' do + User.current = nil + + result = reaction_button(journals(:journals_001)) + + assert_select_in result, 'span.reaction-button.readonly[title=?]', 'John Smith' + assert_select_in result, 'a.reaction-button', false + end + + test 'reaction_button for inactive projects shows readonly button' do + issue6 = issues(:issues_006) + issue6.project.update!(status: Project::STATUS_CLOSED) + + result = reaction_button(issue6) + + assert_select_in result, 'span.reaction-button.readonly[title=?]', 'John Smith' + assert_select_in result, 'a.reaction-button', false + end + + test 'reaction_button includes no tooltip when the object has no reactions' do + issue = issues(:issues_002) # Issue without reactions + result = reaction_button(issue) + + assert_select_in result, 'a.reaction-button[title]', false + end + + test 'reaction_button includes tooltip with all usernames when reactions are 10 or fewer' do + issue = issues(:issues_002) + + reactions = build_reactions(10) + issue.reactions += reactions + + result = with_locale 'en' do + reaction_button(issue) + end + + # The tooltip should display usernames in order of newest reactions. + expected_tooltip = 'Bob9 Doe, Bob8 Doe, Bob7 Doe, Bob6 Doe, Bob5 Doe, ' \ + 'Bob4 Doe, Bob3 Doe, Bob2 Doe, Bob1 Doe, and Bob0 Doe' + + assert_select_in result, 'a.reaction-button[title=?]', expected_tooltip + end + + test 'reaction_button includes tooltip with 10 usernames and others count when reactions exceed 10' do + issue = issues(:issues_002) + + reactions = build_reactions(11) + issue.reactions += reactions + + result = with_locale 'en' do + reaction_button(issue) + end + + expected_tooltip = 'Bob10 Doe, Bob9 Doe, Bob8 Doe, Bob7 Doe, Bob6 Doe, ' \ + 'Bob5 Doe, Bob4 Doe, Bob3 Doe, Bob2 Doe, Bob1 Doe, and 1 other' + + assert_select_in result, 'a.reaction-button[title=?]', expected_tooltip + end + + test 'reaction_button should be label less when no reactions' do + issue = issues(:issues_002) + + result = with_locale('en') do + reaction_button(issue) + end + assert_select_in result, 'a.reaction-button' do + assert_select 'span.icon-label', false + end + + # readonly + User.current = nil + result = with_locale('en') do + reaction_button(issue) + end + assert_select_in result, 'span.reaction-button.readonly' do + assert_select 'span.icon-label', false + end + end + + test 'reaction_button should not count and display non-visible users' do + issue2 = issues(:issues_002) + + issue2.reaction_detail = Reaction::Detail.new( + visible_users: users(:users_002, :users_003) + ) + + result = with_locale('en') do + reaction_button(issue2) + end + + assert_select_in result, 'a.reaction-button[title=?]', 'John Smith and Dave Lopper' + + # When all users are non-visible users + issue2.reaction_detail = Reaction::Detail.new( + visible_users: [] + ) + + result = with_locale('en') do + reaction_button(issue2) + end + + assert_select_in result, 'a.reaction-button[title]', false + assert_select_in result, 'a.reaction-button' do + assert_select 'span.icon-label', false + end + end + + test 'reaction_button formats the tooltip content based on the support.array settings of each locale' do + result = with_locale('ja') do + reaction_button(issues(:issues_001)) + end + + assert_select_in result, 'a.reaction-button[title=?]', 'Dave Lopper、John Smith、Redmine Admin' + end + + test 'reaction_button for reacted object' do + User.current = users(:users_002) + + issue = issues(:issues_001) + + result = with_locale('en') do + reaction_button(issue) + end + tooltip = 'Dave Lopper, John Smith, and Redmine Admin' + + assert_select_in result, 'span.reaction-button-wrapper[data-reaction-button-id=?]', 'reaction_issue_1' do + href = reaction_path(issue.reaction_detail.user_reaction, object_type: 'Issue', object_id: 1) + + assert_select 'a.icon.reaction-button.reacted[href=?]', href do + assert_select 'use[href*=?]', 'thumb-up-filled' + assert_select 'span.icon-label', '3' + end + + assert_select 'span.reaction-button', false + end + end + + test 'reaction_button for non-reacted object' do + User.current = users(:users_004) + + issue = issues(:issues_001) + + result = with_locale('en') do + reaction_button(issue) + end + tooltip = 'Dave Lopper, John Smith, and Redmine Admin' + + assert_select_in result, 'span.reaction-button-wrapper[data-reaction-button-id=?]', 'reaction_issue_1' do + href = reactions_path(object_type: 'Issue', object_id: 1) + + assert_select 'a.icon.reaction-button[href=?]', href do + assert_select 'use[href*=?]', 'thumb-up' + assert_select 'span.icon-label', '3' + end + + assert_select 'span.reaction-button', false + end + end + + private + + def build_reactions(count) + Array.new(count) do |i| + Reaction.new(user: User.generate!(firstname: "Bob#{i}")) + end + end +end diff --git a/test/integration/api_test/news_test.rb b/test/integration/api_test/news_test.rb index bd9f2bb6d..399b2b347 100644 --- a/test/integration/api_test/news_test.rb +++ b/test/integration/api_test/news_test.rb @@ -62,7 +62,7 @@ class Redmine::ApiTest::NewsTest < Redmine::ApiTest::Base assert_select "author[id=2][name=\"John Smith\"]" assert_select 'title', 'eCookbook first release !' assert_select 'summary', 'First version was released...' - assert_select 'description', "eCookbook 1.0 has been released.\n\nVisit http://ecookbook.somenet.foo/" + assert_select 'description', 'eCookbook 1.0 has been released. Visit http://ecookbook.somenet.foo/' assert_select 'created_on', News.find(1).created_on.iso8601 end end diff --git a/test/system/issues_test.rb b/test/system/issues_test.rb index 80ef25e0c..c161538e7 100644 --- a/test/system/issues_test.rb +++ b/test/system/issues_test.rb @@ -34,6 +34,8 @@ class IssuesSystemTest < ApplicationSystemTestCase find('input[name=commit]').click end + assert_text /Issue #\d+ created./ + # find created issue issue = Issue.find_by_subject("new test issue") assert_kind_of Issue, issue @@ -86,6 +88,7 @@ class IssuesSystemTest < ApplicationSystemTestCase fill_in field2.name, :with => 'CF2 value' assert_difference 'Issue.count' do page.first(:button, 'Create').click + assert_text /Issue #\d+ created./ end issue = Issue.order('id desc').first @@ -125,6 +128,7 @@ class IssuesSystemTest < ApplicationSystemTestCase end assert_difference 'Issue.count' do find('input[name=commit]').click + assert_text /Issue #\d+ created./ end issue = Issue.order('id desc').first @@ -141,6 +145,7 @@ class IssuesSystemTest < ApplicationSystemTestCase attach_file 'attachments[dummy][file]', Rails.root.join('test/fixtures/files/testfile.txt') fill_in 'attachments[1][description]', :with => 'Some description' click_on 'Create' + assert_text /Issue #\d+ created./ end assert_equal 1, issue.attachments.count assert_equal 'Some description', issue.attachments.first.description @@ -163,6 +168,7 @@ class IssuesSystemTest < ApplicationSystemTestCase attach_file 'attachments[dummy][file]', Rails.root.join('test/fixtures/files/testfile.txt') fill_in 'attachments[1][description]', :with => 'Some description' click_on 'Create' + assert_text /Issue #\d+ created./ end assert_equal 1, issue.attachments.count assert_equal 'Some description', issue.attachments.first.description @@ -181,6 +187,7 @@ class IssuesSystemTest < ApplicationSystemTestCase click_on 'Create' end click_on 'Create' + assert_text /Issue #\d+ created./ end end @@ -200,6 +207,7 @@ class IssuesSystemTest < ApplicationSystemTestCase end assert_difference 'Issue.count' do click_button('Create') + assert_text /Issue #\d+ created./ end issue = Issue.order('id desc').first @@ -230,6 +238,7 @@ class IssuesSystemTest < ApplicationSystemTestCase fill_in 'Form update CF', :with => 'CF value' assert_no_difference 'Issue.count' do page.first(:button, 'Submit').click + assert_text 'Successful update.' end assert page.has_css?('#flash_notice') issue = Issue.find(1) @@ -245,6 +254,7 @@ class IssuesSystemTest < ApplicationSystemTestCase page.find("#issue_status_id").select("Closed") assert_no_difference 'Issue.count' do page.first(:button, 'Submit').click + assert_text 'Successful update.' end assert page.has_css?('#flash_notice') assert_equal 5, issue.reload.status.id @@ -267,6 +277,7 @@ class IssuesSystemTest < ApplicationSystemTestCase click_on 'Submit' + assert_text 'Successful update.' assert_equal 3, Issue.find(2).attachments.count end diff --git a/test/system/oauth_provider_test.rb b/test/system/oauth_provider_test.rb new file mode 100644 index 000000000..364ed4c94 --- /dev/null +++ b/test/system/oauth_provider_test.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require_relative '../application_system_test_case' +require 'oauth2' +require 'rack' +require 'puma' + +class OauthProviderSystemTest < ApplicationSystemTestCase + fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles, + :trackers, :projects_trackers, :enabled_modules, :issue_statuses, :issues, + :enumerations, :custom_fields, :custom_values, :custom_fields_trackers, + :watchers, :journals, :journal_details, :versions, + :workflows + + test 'application creation and authorization' do + # + # admin creates the application, granting permissions and generating a uuid + # and secret. + # + log_user 'admin', 'admin' + with_settings rest_api_enabled: 1 do + visit '/admin' + within 'div#admin-menu ul' do + click_link 'Applications' + end + click_link 'New Application' + fill_in 'Name', with: 'Oauth Test' + + # as per https://tools.ietf.org/html/rfc8252#section-7.3, the port can be + # anything when the redirect URI's host is 127.0.0.1. + fill_in 'Redirect URI', with: 'http://127.0.0.1' + + check 'View Issues' + click_button 'Create' + + assert_text "Application created." + end + + assert app = Doorkeeper::Application.find_by_name('Oauth Test') + + find 'h2', visible: true, text: /Oauth Test/ + find 'p code', visible: true, text: app.uid + find 'p strong', visible: true, text: /will not be shown again/ + find 'p code', visible: true, text: /View Issues/ + + # scrape the clear text secret from the page + app_secret = all(:css, 'p code')[1].text + + click_link 'Sign out' + + # + # regular user authorizes the application + # + client = OAuth2::Client.new(app.uid, app_secret, site: "http://127.0.0.1:#{test_port}/") + + # set up a dummy http listener to handle the redirect + port = rand 10000..20000 + redirect_uri = "http://127.0.0.1:#{port}" + # the request handler below will set this to the auth token + token = nil + + # launches webrick, listening for the redirect with the auth code. + launch_client_app(port: port) do |req, res| + # get access code from code url param + if code = req.params['code'].presence + # exchange it for token + token = client.auth_code.get_token(code, redirect_uri: redirect_uri) + res.body = ["<html><body><p>Authorization succeeded, you may close this window now.</p></body></html>"] + end + end + + log_user 'jsmith', 'jsmith' + with_settings rest_api_enabled: 1 do + visit '/my/account' + click_link 'Authorized applications' + find 'p.nodata', visible: true + + # an oauth client would send the user to this url to request permission + url = client.auth_code.authorize_url redirect_uri: redirect_uri, scope: 'view_issues view_project' + uri = URI.parse url + visit uri.path + '?' + uri.query + + find 'h2', visible: true, text: 'Authorization required' + find 'p', visible: true, text: /Authorize Oauth Test/ + find '.oauth-permissions', visible: true, text: /View Issues/ + find '.oauth-permissions', visible: true, text: /View project/ + + click_button 'Authorize' + + assert grant = app.access_grants.last + assert_equal 'view_issues view_project', grant.scopes.to_s + + # check for output defined above in the request handler + find 'p', visible: true, text: /Authorization succeeded/ + assert token.present? + + visit '/my/account' + click_link 'Authorized applications' + find 'td', visible: true, text: /Oauth Test/ + click_link 'Sign out' + + # Now, use the token for some API requests + assert_raise(RestClient::Unauthorized) do + RestClient.get "http://localhost:#{test_port}/projects/onlinestore/issues.json" + end + + headers = { 'Authorization' => "Bearer #{token.token}" } + r = RestClient.get "http://localhost:#{test_port}/projects/onlinestore/issues.json", headers + issues = JSON.parse(r.body)['issues'] + assert issues.any? + + # time entries access is not part of the granted scopes + assert_raise(RestClient::Forbidden) do + RestClient.get "http://localhost:#{test_port}/projects/onlinestore/time_entries.json", headers + end + end + end + + private + + def launch_client_app(port: 12345, path: '/', &block) + app = ->(env) do + req = Rack::Request.new(env) + res = Rack::Response.new + yield(req, res) + res.finish + end + + server = Puma::Server.new app + server.add_tcp_listener '127.0.0.1', port + Thread.new { server.run } + end + + def test_port + Capybara.current_session.server.port + end +end diff --git a/test/system/reactions_test.rb b/test/system/reactions_test.rb new file mode 100644 index 000000000..96dd4cf81 --- /dev/null +++ b/test/system/reactions_test.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require_relative '../application_system_test_case' + +class ReactionsSystemTest < ApplicationSystemTestCase + def test_react_to_issue + log_user('jsmith', 'jsmith') + + issue = issues(:issues_002) + + with_settings(reactions_enabled: '1') do + visit '/issues/2' + reaction_button = find("div.issue.details [data-reaction-button-id=\"reaction_issue_#{issue.id}\"]") + assert_reaction_add_and_remove(reaction_button, issue) + end + end + + def test_react_to_journal + log_user('jsmith', 'jsmith') + + journal = journals(:journals_002) + + with_settings(reactions_enabled: '1') do + visit '/issues/1' + reaction_button = find("[data-reaction-button-id=\"reaction_journal_#{journal.id}\"]") + assert_reaction_add_and_remove(reaction_button, journal.reload) + end + end + + def test_react_to_forum_reply + log_user('jsmith', 'jsmith') + + reply_message = messages(:messages_002) # reply to message_001 + + with_settings(reactions_enabled: '1') do + visit 'boards/1/topics/1' + reaction_button = find("[data-reaction-button-id=\"reaction_message_#{reply_message.id}\"]") + assert_reaction_add_and_remove(reaction_button, reply_message) + end + end + + def test_react_to_forum_message + log_user('jsmith', 'jsmith') + + message = messages(:messages_001) + + with_settings(reactions_enabled: '1') do + visit 'boards/1/topics/1' + reaction_button = find("[data-reaction-button-id=\"reaction_message_#{message.id}\"]") + assert_reaction_add_and_remove(reaction_button, message) + end + end + + def test_react_to_news + log_user('jsmith', 'jsmith') + + with_settings(reactions_enabled: '1') do + visit '/news/2' + reaction_button = find("[data-reaction-button-id=\"reaction_news_2\"]") + assert_reaction_add_and_remove(reaction_button, news(:news_002)) + end + end + + def test_react_to_comment + log_user('jsmith', 'jsmith') + + comment = comments(:comments_002) + + with_settings(reactions_enabled: '1') do + visit '/news/1' + reaction_button = find("[data-reaction-button-id=\"reaction_comment_#{comment.id}\"]") + assert_reaction_add_and_remove(reaction_button, comment) + end + end + + def test_reactions_disabled + log_user('jsmith', 'jsmith') + + with_settings(reactions_enabled: '0') do + visit '/issues/1' + assert_no_selector('[data-reaction-button-id="reaction_issue_1"]') + end + end + + def test_reaction_button_is_visible_but_not_clickable_for_not_logged_in_user + with_settings(reactions_enabled: '1') do + visit '/issues/1' + + # visible + reaction_button = find('div.issue.details [data-reaction-button-id="reaction_issue_1"]') + within(reaction_button) { assert_selector('span.reaction-button') } + assert_equal "3", reaction_button.text + + # not clickable + within(reaction_button) { assert_no_selector('a.reaction-button') } + end + end + + def test_reaction_button_is_visible_on_property_changes_tab + # Create a journal with no notes + journal_without_notes = Journal.generate!(journalized: issues(:issues_001), notes: '', details: [JournalDetail.new]) + + log_user('jsmith', 'jsmith') + + visit '/issues/1?tab=properties' + + # Scroll to the history content + click_link '#1' + + assert_selector '#tab-properties.selected' + + within('#change-1') do + assert_selector 'a.reaction-button' + + assert_no_selector 'a.icon-quote' + assert_no_selector 'span.drdn' + end + within("#change-#{journal_without_notes.id}") do + assert_selector 'a.reaction-button' + + assert_no_selector '.drdn' + end + + click_link 'History' + + within('#change-1') do + assert_selector 'a.reaction-button' + + assert_selector 'a.icon-quote' + assert_selector 'span.drdn' + end + within("#change-#{journal_without_notes.id}") do + assert_selector 'a.reaction-button' + assert_selector 'span.drdn' + + assert_no_selector 'a.icon-quote' + end + end + + private + + def assert_reaction_add_and_remove(reaction_button, expected_subject) + # Add a reaction + within(reaction_button) { find('a.reaction-button').click } + find('body').hover # Hide tooltip + within(reaction_button) { assert_selector('a.reaction-button.reacted[title="John Smith"]') } + assert_equal "1", reaction_button.text + assert_equal 1, expected_subject.reactions.count + + # Remove the reaction + within(reaction_button) { find('a.reacted').click } + within(reaction_button) { assert_selector('a.reaction-button:not(.reacted)') } + assert_equal "", reaction_button.text + assert_equal 0, expected_subject.reactions.count + end +end diff --git a/test/system/sticky_issue_header_test.rb b/test/system/sticky_issue_header_test.rb new file mode 100644 index 000000000..5c67b36f3 --- /dev/null +++ b/test/system/sticky_issue_header_test.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +require_relative '../application_system_test_case' +class StickyIssueHeaderSystemTest < ApplicationSystemTestCase + test "sticky issue header is hidden by default" do + issue = Issue.find(1) + visit issue_path(issue) + + assert_no_selector "#sticky-issue-header", text: issue.subject + end + + test "sticky issue header appears on scroll" do + issue = Issue.find(2) + visit issue_path(issue) + + page.execute_script("window.scrollTo(0, 1000)") + assert_selector "#sticky-issue-header.is-visible", text: issue.subject + + page.execute_script("window.scrollTo(0, 0)") + assert_no_selector "#sticky-issue-header", text: issue.subject + end +end diff --git a/test/system/sudo_mode_test.rb b/test/system/sudo_mode_test.rb index 73e755acd..307d465ff 100644 --- a/test/system/sudo_mode_test.rb +++ b/test/system/sudo_mode_test.rb @@ -48,7 +48,6 @@ class SudoModeSystemTest < ApplicationSystemTestCase find('input[name=commit]').click end - assert_equal '/users', current_path assert page.has_content?("Confirm your password to continue") assert page.has_css?('form#sudo-form') @@ -56,6 +55,8 @@ class SudoModeSystemTest < ApplicationSystemTestCase fill_in 'Password', :with => 'admin' click_button 'Submit' end + + assert_text /User johnpaul created./ end end diff --git a/test/system/timelog_test.rb b/test/system/timelog_test.rb index 57c521096..38c3ae19c 100644 --- a/test/system/timelog_test.rb +++ b/test/system/timelog_test.rb @@ -49,6 +49,8 @@ class TimelogTest < ApplicationSystemTestCase select 'QA', :from => 'Activity' page.first(:button, 'Submit').click + assert_text 'Successful update.' + entries = TimeEntry.where(:id => [1, 2, 3]).to_a assert entries.all? {|entry| entry.hours == 8.5} assert entries.all? {|entry| entry.activity.name == 'QA'} @@ -89,6 +91,7 @@ class TimelogTest < ApplicationSystemTestCase select 'Tracker', :from => 'Available Columns' page.first('input[type=button].move-right').click click_on 'Save' + assert_text 'Successful update.' # Display the list with updated settings visit '/time_entries' diff --git a/test/unit/lib/redmine/field_format/progressbar_format_test.rb b/test/unit/lib/redmine/field_format/progressbar_format_test.rb index 51bd8bc5f..6e0df724d 100644 --- a/test/unit/lib/redmine/field_format/progressbar_format_test.rb +++ b/test/unit/lib/redmine/field_format/progressbar_format_test.rb @@ -116,5 +116,17 @@ module Redmine::FieldFormat end end end + + def test_formatted_value_with_html_true + expected = progress_bar(50) + formatted = @format.formatted_value(self, @field, 50, Issue.new, true) + assert_equal expected, formatted + assert formatted.html_safe? + end + + def test_formatted_value_with_html_false + formatted = @format.formatted_value(self, @field, 50, Issue.new, false) + assert_equal '50', formatted + end end end diff --git a/test/unit/lib/redmine/quote_reply_helper_test.rb b/test/unit/lib/redmine/quote_reply_helper_test.rb index 43adb521b..cbac1f6d0 100644 --- a/test/unit/lib/redmine/quote_reply_helper_test.rb +++ b/test/unit/lib/redmine/quote_reply_helper_test.rb @@ -29,7 +29,7 @@ class QuoteReplyHelperTest < ActionView::TestCase a_tag = quote_reply(url, '#issue_description_wiki') assert_includes a_tag, %|onclick="#{h "quoteReply('/issues/1/quoted', '#issue_description_wiki', 'common_mark'); return false;"}"| - assert_includes a_tag, %|class="icon icon-comment"| + assert_includes a_tag, %|class="icon icon-quote"| assert_not_includes a_tag, 'title=' # When icon_only is true diff --git a/test/unit/lib/redmine/reaction_test.rb b/test/unit/lib/redmine/reaction_test.rb new file mode 100644 index 000000000..f3228a3bd --- /dev/null +++ b/test/unit/lib/redmine/reaction_test.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require_relative '../../../test_helper' + +class Redmine::ReactionTest < ActiveSupport::TestCase + setup do + @user = users(:users_002) + @issue = issues(:issues_007) + Setting.reactions_enabled = '1' + end + + teardown do + Setting.clear_cache + end + + test 'preload_reaction_details preloads ReactionDetail for all objects in the collection' do + User.current = users(:users_002) + + issue1 = issues(:issues_001) + issue2 = issues(:issues_002) + + assert_nil issue1.instance_variable_get(:@reaction_detail) + assert_nil issue2.instance_variable_get(:@reaction_detail) + + Issue.preload_reaction_details([issue1, issue2]) + + expected_issue1_reaction_detail = Reaction::Detail.new( + visible_users: [users(:users_003), users(:users_002), users(:users_001)], + user_reaction: reactions(:reaction_002) + ) + + # ReactionDetail is already preloaded, so calling reaction_detail does not execute any query. + assert_no_queries do + assert_equal expected_issue1_reaction_detail, issue1.reaction_detail + + # Even when an object has no reactions, an empty ReactionDetail is set. + assert_equal Reaction::Detail.new( + visible_users: [], + user_reaction: nil + ), issue2.reaction_detail + end + end + + test 'visible_users in ReactionDetail preloaded by preload_reaction_details does not include non-visible users' do + current_user = User.current = User.generate! + visible_user = users(:users_002) + non_visible_user = User.generate! + + project = Project.generate! + role = Role.generate!(users_visibility: 'members_of_visible_projects') + + User.add_to_project(current_user, project, role) + User.add_to_project(visible_user, project, roles(:roles_001)) + + issue = Issue.generate!(project: project) + + [current_user, visible_user, non_visible_user].each do |user| + issue.reactions.create!(user: user) + end + + Issue.preload_reaction_details([issue]) + + # non_visible_user is not visible to current_user because they do not belong to any project. + assert_equal [visible_user, current_user], issue.reaction_detail.visible_users + end + + test 'preload_reaction_details does nothing when the reaction feature is disabled' do + Setting.reactions_enabled = '0' + + User.current = users(:users_002) + news1 = news(:news_001) + + # Stub the Setting to avoid executing queries for retrieving settings, + # making it easier to confirm no queries are executed by preload_reaction_details(). + Setting.stubs(:reactions_enabled?).returns(false) + + assert_no_queries do + News.preload_reaction_details([news1]) + end + + assert_nil news1.instance_variable_get(:@reaction_detail) + end + + test 'reaction_detail loads and returns ReactionDetail if it is not preloaded' do + message7 = messages(:messages_007) + + User.current = users(:users_002) + assert_nil message7.instance_variable_get(:@reaction_detail) + + assert_equal Reaction::Detail.new( + visible_users: [users(:users_002)], + user_reaction: reactions(:reaction_009) + ), message7.reaction_detail + end + + test 'load_reaction_detail loads ReactionDetail for the object itself' do + comment1 = comments(:comments_001) + + User.current = users(:users_001) + assert_nil comment1.instance_variable_get(:@reaction_detail) + + comment1.load_reaction_detail + + assert_equal Reaction::Detail.new( + visible_users: [users(:users_002)], + user_reaction: nil + ), comment1.reaction_detail + end + + test 'visible? returns true when reactions are enabled and object is visible to user' do + object = issues(:issues_007) + user = users(:users_002) + + assert Redmine::Reaction.visible?(object, user) + end + + test 'visible? returns false when reactions are disabled' do + Setting.reactions_enabled = '0' + + object = issues(:issues_007) + user = users(:users_002) + + assert_not Redmine::Reaction.visible?(object, user) + end + + test 'visible? returns false when object is not visible to user' do + object = issues(:issues_007) + user = users(:users_002) + + object.expects(:visible?).with(user).returns(false) + + assert_not Redmine::Reaction.visible?(object, user) + end + + test 'editable? returns true for various reactable objects when user is logged in, object is visible, and project is active' do + reactable_objects = { + issue: issues(:issues_007), + message: messages(:messages_001), + news: news(:news_001), + journal: journals(:journals_001), + comment: comments(:comments_002) + } + user = users(:users_002) + + reactable_objects.each do |type, object| + assert Redmine::Reaction.editable?(object, user), "Expected editable? to return true for #{type}" + end + end + + test 'editable? returns false when user is not logged in' do + object = issues(:issues_007) + user = User.anonymous + + assert_not Redmine::Reaction.editable?(object, user) + end + + test 'editable? returns false when project is inactive' do + object = issues(:issues_007) + user = users(:users_002) + object.project.update!(status: Project::STATUS_ARCHIVED) + + assert_not Redmine::Reaction.editable?(object, user) + end + + test 'editable? returns false when project is closed' do + object = issues(:issues_007) + user = users(:users_002) + object.project.update!(status: Project::STATUS_CLOSED) + + assert_not Redmine::Reaction.editable?(object, user) + end +end diff --git a/test/unit/lib/redmine/wiki_formatting/common_mark/formatter_test.rb b/test/unit/lib/redmine/wiki_formatting/common_mark/formatter_test.rb index 11d5913ce..bb0c5d450 100644 --- a/test/unit/lib/redmine/wiki_formatting/common_mark/formatter_test.rb +++ b/test/unit/lib/redmine/wiki_formatting/common_mark/formatter_test.rb @@ -163,39 +163,39 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase # 0 <<~STR.chomp, # Title - + Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero. STR # 1 <<~STR.chomp, ## Heading 2 - + ~~~ruby def foo end ~~~ - + Morbi facilisis accumsan orci non pharetra. - + ~~~ ruby def foo end ~~~ - + ``` Pre Content: - + ## Inside pre - + <tag> inside pre block - + Morbi facilisis accumsan orci non pharetra. ``` STR # 2 <<~STR.chomp, ### Heading 3 - + Nulla nunc nisi, egestas in ornare vel, posuere ac libero. STR ] @@ -226,7 +226,7 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase text = STR_WITH_PRE.join("\n\n") replacement = "New text" - assert_equal [STR_WITH_PRE[0..1], "New text"].flatten.join("\n\n"), + assert_equal [STR_WITH_PRE[0..1], "New text"].join("\n\n"), @formatter.new(text).update_section(3, replacement) end @@ -287,7 +287,7 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase expected = <<~EXPECTED <p>Task list:</p> - <ul class="task-list"> + <ul class="contains-task-list"> <li class="task-list-item"> <input type="checkbox" class="task-list-item-checkbox" disabled> Task 1 </li> @@ -299,6 +299,49 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase assert_equal expected.gsub(%r{[\r\n\t]}, ''), to_html(text).gsub(%r{[\r\n\t]}, '').rstrip end + def test_should_render_alert_blocks + text = <<~MD + > [!note] + > This is a note. + + > [!tip] + > This is a tip. + + > [!warning] + > This is a warning. + + > [!caution] + > This is a caution. + + > [!important] + > This is a important. + MD + + html = to_html(text) + %w[note tip warning caution important].each do |alert| + icon = Redmine::WikiFormatting::CommonMark::ALERT_TYPE_TO_ICON_NAME[alert] + # rubocop:disable Layout/LineLength + expected = %r{<div class="markdown-alert markdown-alert-#{alert}">\n<p class="markdown-alert-title"><svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/icons-\w+.svg\#icon--#{icon}"></use></svg><span class="icon-label">#{alert.capitalize}</span></p>\n<p>This is a #{alert}.</p>\n</div>} + # rubocop:enable Layout/LineLength + assert_match expected, html + end + end + + def test_should_not_render_unknown_alert_type + text = <<~MD + > [!unknown] + > This should not become an alert. + MD + + html = to_html(text) + + assert_include "<blockquote>", html + assert_include "[!unknown]", html + assert_include "This should not become an alert.", html + + assert_not_include 'markdown-alert', html + end + private def assert_section_with_hash(expected, text, index) diff --git a/test/unit/lib/redmine/wiki_formatting/common_mark/sanitization_filter_test.rb b/test/unit/lib/redmine/wiki_formatting/common_mark/sanitization_filter_test.rb index 4c0282f2d..b2d19eab9 100644 --- a/test/unit/lib/redmine/wiki_formatting/common_mark/sanitization_filter_test.rb +++ b/test/unit/lib/redmine/wiki_formatting/common_mark/sanitization_filter_test.rb @@ -47,10 +47,14 @@ if Object.const_defined?(:Commonmarker) end def test_should_support_footnotes - input = %(<a href="#fn-1" id="fnref-1">foo</a>) - assert_equal input, filter(input) - input = %(<ol><li id="fn-1">footnote</li></ol>) - assert_equal input, filter(input) + [ + %(<a href="#fn-1" id="fnref-1">foo</a>), + %(<a href="#fn-1" id="fnref-1-2">foo</a>), + %(<ol><li id="fn-1">footnote</li></ol>), + ].each do |input| + assert_equal input, filter(input) + assert_equal input, filter(input) + end end def test_should_remove_invalid_ids @@ -71,6 +75,32 @@ if Object.const_defined?(:Commonmarker) assert_equal %(<code>foo</code>), filter(input) end + def test_should_allow_valid_alert_div_and_p_classes + html = <<~HTML + <div class="markdown-alert markdown-alert-tip"> + <p class="markdown-alert-title">Tip</p> + <p>Useful tip.</p> + </div> + HTML + + sanitized = filter(html) + + assert_include 'class="markdown-alert markdown-alert-tip"', sanitized + assert_include 'class="markdown-alert-title"', sanitized + end + + def test_should_remove_invalid_div_class + html = '<div class="bad-class">Text</div>' + sanitized = filter(html) + assert_not_includes 'bad-class', sanitized + end + + def test_should_remove_invalid_p_class + html = '<p class="bad-class">Text</p>' + sanitized = filter(html) + assert_not_include 'bad-class', sanitized + end + def test_should_allow_links_with_safe_url_schemes %w(http https ftp ssh foo).each do |scheme| input = %(<a href="#{scheme}://example.org/">foo</a>) diff --git a/test/unit/lib/redmine/wiki_formatting/html_sanitizer_test.rb b/test/unit/lib/redmine/wiki_formatting/html_sanitizer_test.rb index 11dddb5f8..f8793cf9f 100644 --- a/test/unit/lib/redmine/wiki_formatting/html_sanitizer_test.rb +++ b/test/unit/lib/redmine/wiki_formatting/html_sanitizer_test.rb @@ -35,4 +35,24 @@ class Redmine::WikiFormatting::HtmlSanitizerTest < ActiveSupport::TestCase input = %(<a href="javascript:alert('hello');">foo</a>) assert_equal "<a>foo</a>", @sanitizer.call(input) end + + def test_should_be_strict_with_task_list_items + to_test = { + %(<input type="checkbox" class="">) => "", + %(<input type="checkbox" class="task-list-item-checkbox other">) => "", + %(<input type="checkbox" class="task-list-item-checkbox" id="item1">) => %(<input type="checkbox" class="task-list-item-checkbox">), + %(<input type="text" class="">) => "", + %(<input />) => "", + %(<ul class="other"></ul) => "<ul></ul>", + %(<ul class="contains-task-list"></ul) => "<ul class=\"contains-task-list\"></ul>", + %(<ul class="contains-task-list" id="list1"></ul) => "<ul class=\"contains-task-list\"></ul>", + %(<li class="other"></li>) => "", + %(<li id="other"></li>) => "", + %(<li class="task-list-item"></li>) => "", + %(<li class="task-list-item">Item 1</li>) => "Item 1", + } + to_test.each do |input, result| + assert_equal result, @sanitizer.call(input) + end + end end diff --git a/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb b/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb index 32280cfdf..678d4c6b2 100644 --- a/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb +++ b/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb @@ -466,19 +466,19 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase replacement = "New text" assert_equal( - [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"), + [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement) ) assert_equal( - [STR_WITHOUT_PRE[0..1], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"), + [STR_WITHOUT_PRE[0..1], replacement, STR_WITHOUT_PRE[4]].join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(3, replacement) ) assert_equal( - [STR_WITHOUT_PRE[0..2], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"), + [STR_WITHOUT_PRE[0..2], replacement, STR_WITHOUT_PRE[4]].join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(5, replacement) ) assert_equal( - [STR_WITHOUT_PRE[0..3], replacement].flatten.join("\n\n"), + [STR_WITHOUT_PRE[0..3], replacement].join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(6, replacement) ) assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(0, replacement) @@ -488,7 +488,7 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase def test_update_section_with_hash_should_update_the_requested_section replacement = "New text" assert_equal( - [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"), + [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE). update_section(2, replacement, ActiveSupport::Digest.hexdigest(STR_WITHOUT_PRE[1])) ) @@ -552,7 +552,7 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase text = STR_WITH_PRE.join("\n\n") replacement = "New text" assert_equal( - [STR_WITH_PRE[0..1], "New text"].flatten.join("\n\n"), + [STR_WITH_PRE[0..1], "New text"].join("\n\n"), @formatter.new(text).update_section(3, replacement) ) end diff --git a/test/unit/member_test.rb b/test/unit/member_test.rb index 42fba4783..df9088027 100644 --- a/test/unit/member_test.rb +++ b/test/unit/member_test.rb @@ -108,7 +108,7 @@ class MemberTest < ActiveSupport::TestCase assert !member.save assert_include I18n.translate('activerecord.errors.messages.empty'), member.errors[:role] assert_equal 'Rôle doit être renseigné(e)', - [member.errors.full_messages].flatten.join + [member.errors.full_messages].join end def test_validate_member_role diff --git a/test/unit/reaction_test.rb b/test/unit/reaction_test.rb new file mode 100644 index 000000000..9b3da0738 --- /dev/null +++ b/test/unit/reaction_test.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require_relative '../test_helper' + +class ReactionTest < ActiveSupport::TestCase + test 'validates :inclusion of reactable_type' do + %w(Issue Journal News Comment Message).each do |type| + reaction = Reaction.new(reactable_type: type, user: User.new) + assert reaction.valid? + end + + assert_not Reaction.new(reactable_type: 'InvalidType', user: User.new).valid? + end + + test 'scope: by' do + user2_reactions = issues(:issues_001).reactions.by(users(:users_002)) + + assert_equal [reactions(:reaction_002)], user2_reactions + end + + test "should prevent duplicate reactions with unique constraint under concurrent creation" do + user = users(:users_001) + issue = issues(:issues_004) + + threads = [] + results = [] + + # Ensure both threads start at the same time + barrier = Concurrent::CyclicBarrier.new(2) + + # Create two threads to simulate concurrent creation + 2.times do + threads << Thread.new do + barrier.wait # Wait for both threads to be ready + begin + reaction = Reaction.create( + reactable: issue, + user: user + ) + results << reaction.persisted? + rescue ActiveRecord::RecordNotUnique + results << false + end + end + end + + # Wait for both threads to finish + threads.each(&:join) + + # Ensure only one reaction was created + assert_equal 1, Reaction.where(reactable: issue, user: user).count + assert_includes results, true + assert_equal 1, results.count(true) + end + + test 'build_detail_map_for generates a detail map for reactable objects' do + result = Reaction.build_detail_map_for([issues(:issues_001), issues(:issues_006)], users(:users_003)) + + expected = { + 1 => Reaction::Detail.new( + visible_users: [users(:users_003), users(:users_002), users(:users_001)], + user_reaction: reactions(:reaction_003) + ), + 6 => Reaction::Detail.new( + visible_users: [users(:users_002)], + user_reaction: nil + ) + } + assert_equal expected, result + + # When an object have no reactions, the result should be empty. + result = Reaction.build_detail_map_for([journals(:journals_002)], users(:users_002)) + + assert_empty result + end + + test 'build_detail_map_for filters users based on visibility' do + current_user = User.generate! + visible_user = users(:users_002) + non_visible_user = User.generate! + + project = Project.generate! + role = Role.generate!(users_visibility: 'members_of_visible_projects') + + User.add_to_project(current_user, project, role) + User.add_to_project(visible_user, project, roles(:roles_001)) + + issue = Issue.generate!(project: project) + + [current_user, visible_user, non_visible_user].each do |user| + issue.reactions.create!(user: user) + end + + result = Reaction.build_detail_map_for([issue], current_user) + + assert_equal( + [current_user, visible_user].sort_by(&:id), + result[issue.id].visible_users.sort_by(&:id) + ) + end +end diff --git a/test/unit/role_test.rb b/test/unit/role_test.rb index 21103919f..1d0d39d7e 100644 --- a/test/unit/role_test.rb +++ b/test/unit/role_test.rb @@ -175,6 +175,32 @@ class RoleTest < ActiveSupport::TestCase assert_equal false, role.permissions_tracker_ids?(:view_issues, 1) end + def test_allowed_to_with_symbol + role = Role.create!(:name => 'Test', :permissions => [:view_issues]) + assert_equal true, role.allowed_to?(:view_issues) + assert_equal false, role.allowed_to?(:add_issues) + end + + def test_allowed_to_with_symbol_and_scope + role = Role.create!(:name => 'Test', :permissions => [:view_issues, :delete_issues]) + assert_equal true, role.allowed_to?(:view_issues, [:view_issues, :add_issues]) + assert_equal false, role.allowed_to?(:add_issues, [:view_issues, :add_issues]) + assert_equal false, role.allowed_to?(:delete_issues, [:view_issues, :add_issues]) + end + + def test_allowed_to_with_hash + role = Role.create!(:name => 'Test', :permissions => [:view_issues]) + assert_equal true, role.allowed_to?(:controller => 'issues', :action => 'show') + assert_equal false, role.allowed_to?(:controller => 'issues', :action => 'create') + end + + def test_allowed_to_with_hash_and_scope + role = Role.create!(:name => 'Test', :permissions => [:view_issues, :delete_issues]) + assert_equal true, role.allowed_to?({:controller => 'issues', :action => 'show'}, [:view_issues, :add_issues]) + assert_equal false, role.allowed_to?({:controller => 'issues', :action => 'create'}, [:view_issues, :add_issues]) + assert_equal false, role.allowed_to?({:controller => 'issues', :action => 'destroy'}, [:view_issues, :add_issues]) + end + def test_has_permission_without_permissions role = Role.create!(:name => 'Test') assert_equal false, role.has_permission?(:delete_issues) diff --git a/test/unit/setting_test.rb b/test/unit/setting_test.rb index 4ae07cebb..cbfabbb02 100644 --- a/test/unit/setting_test.rb +++ b/test/unit/setting_test.rb @@ -147,4 +147,8 @@ class SettingTest < ActiveSupport::TestCase def test_default_text_formatting_for_new_installations_is_common_mark assert_equal 'common_mark', Setting.text_formatting end + + def test_default_wiki_tablesort_enabled_for_new_installations_is_disabled + assert_equal "0", Setting.wiki_tablesort_enabled + end end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index ede12e1ce..967771c87 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -589,6 +589,27 @@ class UserTest < ActiveSupport::TestCase end end + def test_initials_format + assert_equal 'JS', @jsmith.initials(:firstname_lastinitial) + assert_equal 'SJ', @jsmith.initials(:lastname_comma_firstname) + assert_equal 'SJ', @jsmith.initials(:lastname_firstname) + assert_equal 'JS', @jsmith.initials(:firstinitial_lastname) + assert_equal 'JL', User.new(:firstname => 'Jean-Philippe', :lastname => 'Lang').initials(:firstinitial_lastname) + assert_equal 'JS', @jsmith.initials(:undefined_format) + end + + def test_initials_should_use_setting_as_default_format + with_settings :user_format => :firstname_lastname do + assert_equal 'JS', @jsmith.reload.initials + end + with_settings :user_format => :username do + assert_equal 'JS', @jsmith.reload.initials + end + with_settings :user_format => :lastname do + assert_equal 'SM', @jsmith.reload.initials + end + end + def test_lastname_should_accept_255_characters u = User.first u.lastname = 'a' * 255 @@ -1376,4 +1397,77 @@ class UserTest < ActiveSupport::TestCase User.prune(7) end end + + def test_should_recognize_authorized_by_oauth + u = User.find 2 + assert_not u.authorized_by_oauth? + u.oauth_scope = [:add_issues, :view_issues] + assert u.authorized_by_oauth? + end + + def test_admin_should_be_limited_by_oauth_scope + u = User.find_by_admin(true) + assert u.admin? + + u.oauth_scope = [:add_issues, :view_issues] + assert_not u.admin? + + u.oauth_scope = [:add_issues, :view_issues, :admin] + assert u.admin? + + u = User.find_by_admin(false) + assert_not u.admin? + u.oauth_scope = [:add_issues, :view_issues, :admin] + assert_not u.admin? + end + + def test_oauth_scope_should_limit_global_user_permissions + admin = User.find 1 + user = User.find 2 + [admin, user].each do |u| + assert u.allowed_to?(:add_issues, nil, global: true) + assert u.allowed_to?(:view_issues, nil, global: true) + u.oauth_scope = [:view_issues] + assert_not u.allowed_to?(:add_issues, nil, global: true) + assert u.allowed_to?(:view_issues, nil, global: true) + end + end + + def test_oauth_scope_should_limit_project_user_permissions + admin = User.find 1 + project = Project.find 5 + assert admin.allowed_to?(:add_issues, project) + assert admin.allowed_to?(:view_issues, project) + admin.oauth_scope = [:view_issues] + assert_not admin.allowed_to?(:add_issues, project) + assert admin.allowed_to?(:view_issues, project) + + admin.oauth_scope = [:view_issues, :admin] + assert admin.allowed_to?(:add_issues, project) + assert admin.allowed_to?(:view_issues, project) + + user = User.find 2 + project = Project.find 1 + assert user.allowed_to?(:add_issues, project) + assert user.allowed_to?(:view_issues, project) + user.oauth_scope = [:view_issues] + assert_not user.allowed_to?(:add_issues, project) + assert user.allowed_to?(:view_issues, project) + + user.oauth_scope = [:view_issues, :admin] + assert_not user.allowed_to?(:add_issues, project) + assert user.allowed_to?(:view_issues, project) + end + + def test_destroy_should_delete_associated_reactions + users(:users_004).reactions.create!( + [ + {reactable: issues(:issues_001)}, + {reactable: issues(:issues_002)} + ] + ) + assert_difference 'Reaction.count', -2 do + users(:users_004).destroy + end + end end |