Merge branch 'dev' into merge-release/17.0-20251204132109

This commit is contained in:
Andrej
2025-12-04 14:23:00 +01:00
committed by GitHub
86 changed files with 2439 additions and 1175 deletions
+140 -135
View File
@@ -59,6 +59,9 @@ Layout/MultilineMethodCallIndentation:
Layout/MultilineOperationIndentation:
Enabled: false
Lint/AmbiguousBlockAssociation:
AllowedMethods: [change]
Lint/AmbiguousOperator:
Enabled: false
@@ -98,14 +101,11 @@ Lint/UnderscorePrefixedVariableName:
Lint/Void:
Enabled: false
Lint/AmbiguousBlockAssociation:
AllowedMethods: [change]
Metrics/ClassLength:
Enabled: false
Metrics/CyclomaticComplexity:
Enabled: false
Metrics/AbcSize:
Enabled: true
Exclude:
- "spec/**/*.rb"
- "modules/*/spec/**/*.rb"
Metrics/BlockLength:
Enabled: false
@@ -113,6 +113,12 @@ Metrics/BlockLength:
Metrics/BlockNesting:
Enabled: false
Metrics/ClassLength:
Enabled: false
Metrics/CyclomaticComplexity:
Enabled: false
Metrics/MethodLength:
Enabled: false
@@ -122,12 +128,6 @@ Metrics/ModuleLength:
Metrics/ParameterLists:
Enabled: false
Metrics/AbcSize:
Enabled: true
Exclude:
- "spec/**/*.rb"
- "modules/*/spec/**/*.rb"
Naming/AccessorMethodName:
Enabled: false
@@ -150,10 +150,15 @@ Naming/VariableNumber:
- '\w_20\d\d' # allow dates like christmas_2022 or date_2034_04_12
- '\w\d++(_\d++)+' # allow hierarchical data like child1_2_5 (second + in regex is possessive qualifier)
- 'custom_field_\d+' # allow custom field method names to be called with send :custom_field_1001
# There are valid cases in which to use methods like:
# * update_all
# * touch_all
Rails/SkipsModelValidations:
OpenProject/AddPreviewForViewComponent:
Include:
- app/components/op_turbo/**.rb
- app/components/op_primer/**.rb
- app/components/open_project/**.rb
- app/components/concerns/**.rb
Performance/Casecmp:
Enabled: false
# Don't force us to use tag instead of content_tag
@@ -161,55 +166,6 @@ Rails/SkipsModelValidations:
Rails/ContentTag:
Enabled: false
# Disable I18n.locale = in specs, where it is reset
# by us explicitly
Rails/I18nLocaleAssignment:
Enabled: true
Exclude:
- "spec/**/*.rb"
# Do not bother if `let` statements use an index in their name
RSpec/IndexedLet:
Enabled: false
# The http verbs in Rack::Test do not accept named parameters (params: params)
Rails/HttpPositionalArguments:
Enabled: false
# require_dependency is an obsolete method for Rails applications running in Zeitwerk mode.
Rails/RequireDependency:
Enabled: true
# For feature specs, we tend to have longer specs that cover a larger part of the functionality.
# This is done for multiple reasons:
# * performance, as setting up integration tests is costly
# * following a scenario that is closer to how a user interacts
RSpec/ExampleLength:
Max: 25
Enabled: true
Exclude:
- "spec/features/**/*.rb"
- "modules/*/spec/features/**/*.rb"
# We have specs that have no expect(..) syntax,
# but only helper classes that expect themselves
RSpec/NoExpectationExample:
Enabled: false
RSpec/DescribeClass:
Enabled: true
Exclude:
- "spec/features/**/*.rb"
- "modules/*/spec/features/**/*.rb"
# Nothing wrong with `include_examples` when used properly.
RSpec/IncludeExamples:
Enabled: false
# Allow number HTTP status codes in specs
RSpecRails/HttpStatus:
Enabled: false
# dynamic finders cop clashes with capybara ID cop
Rails/DynamicFindBy:
Enabled: true
@@ -230,53 +186,38 @@ Rails/FindEach:
- select
- lock
# The http verbs in Rack::Test do not accept named parameters (params: params)
Rails/HttpPositionalArguments:
Enabled: false
# Disable I18n.locale = in specs, where it is reset
# by us explicitly
Rails/I18nLocaleAssignment:
Enabled: true
Exclude:
- "spec/**/*.rb"
# We have config.active_record.belongs_to_required_by_default = false ,
# which means, we do have to declare presence validators on belongs_to relations.
Rails/RedundantPresenceValidationOnBelongsTo:
Enabled: false
# See RSpec/ExampleLength for why feature specs are excluded
RSpec/MultipleExpectations:
Max: 15
Enabled: true
Exclude:
- "spec/features/**/*.rb"
- "modules/*/spec/features/**/*.rb"
RSpec/MultipleMemoizedHelpers:
Enabled: false
RSpec/NestedGroups:
Enabled: false
# Don't force the second argument of describe
# to be .class_method or #instance_method
RSpec/DescribeMethod:
Enabled: false
# Don't force the second argument of describe
# to match the exact file name
RSpec/SpecFilePathFormat:
CustomTransform:
OpenIDConnect: openid_connect
OAuthClients: oauth_clients
OAuth: oauth
ICal: ical
IgnoreMethods: true
# Prevent "fit" or similar to be committed
RSpec/Focus:
# require_dependency is an obsolete method for Rails applications running in Zeitwerk mode.
Rails/RequireDependency:
Enabled: true
# We use let!() to ensure dependencies are created
# instead of let() and referencing them explicitly
RSpec/LetSetup:
# Require save! to prevent saving without validation when saving outside of a condition.
Rails/SaveBang:
Enabled: true
# There are valid cases in which to use methods like:
# * update_all
# * touch_all
Rails/SkipsModelValidations:
Enabled: false
RSpec/LeadingSubject:
Enabled: false
RSpec/NamedSubject:
# Allow number HTTP status codes in specs
RSpecRails/HttpStatus:
Enabled: false
# expect not_to change is not working as expected
@@ -303,6 +244,80 @@ RSpec/ContextWording:
- within
- without
RSpec/DescribeClass:
Enabled: true
Exclude:
- "spec/features/**/*.rb"
- "modules/*/spec/features/**/*.rb"
# Don't force the second argument of describe
# to be .class_method or #instance_method
RSpec/DescribeMethod:
Enabled: false
# For feature specs, we tend to have longer specs that cover a larger part of the functionality.
# This is done for multiple reasons:
# * performance, as setting up integration tests is costly
# * following a scenario that is closer to how a user interacts
RSpec/ExampleLength:
Max: 25
Enabled: true
Exclude:
- "spec/features/**/*.rb"
- "modules/*/spec/features/**/*.rb"
# Prevent "fit" or similar to be committed
RSpec/Focus:
Enabled: true
# Nothing wrong with `include_examples` when used properly.
RSpec/IncludeExamples:
Enabled: false
# Do not bother if `let` statements use an index in their name
RSpec/IndexedLet:
Enabled: false
RSpec/LeadingSubject:
Enabled: false
# We use let!() to ensure dependencies are created
# instead of let() and referencing them explicitly
RSpec/LetSetup:
Enabled: false
# We have specs that have no expect(..) syntax,
# but only helper classes that expect themselves
RSpec/NoExpectationExample:
Enabled: false
# See RSpec/ExampleLength for why feature specs are excluded
RSpec/MultipleExpectations:
Max: 15
Enabled: true
Exclude:
- "spec/features/**/*.rb"
- "modules/*/spec/features/**/*.rb"
RSpec/MultipleMemoizedHelpers:
Enabled: false
RSpec/NestedGroups:
Enabled: false
# Don't force the second argument of describe
# to match the exact file name
RSpec/SpecFilePathFormat:
CustomTransform:
OpenIDConnect: openid_connect
OAuthClients: oauth_clients
OAuth: oauth
ICal: ical
IgnoreMethods: true
RSpec/NamedSubject:
Enabled: false
Style/Alias:
Enabled: false
@@ -367,6 +382,19 @@ Style/FormatString:
Style/FormatStringToken:
AllowedMethods: [redirect]
Style/FrozenStringLiteralComment:
Enabled: true
EnforcedStyle: always_true
Style/HashEachMethods:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
Style/GlobalVars:
Enabled: false
@@ -409,6 +437,13 @@ Style/NilComparison:
Style/Not:
Enabled: false
Style/NumericLiterals:
Enabled: false
# Avoid enforcing "positive?"
Style/NumericPredicate:
Enabled: false
Style/OneLineConditional:
Enabled: false
@@ -471,33 +506,3 @@ Style/WhileUntilModifier:
Style/WordArray:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: true
EnforcedStyle: always_true
Style/NumericLiterals:
Enabled: false
# Avoid enforcing "positive?"
Style/NumericPredicate:
Enabled: false
Style/HashEachMethods:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
Performance/Casecmp:
Enabled: false
OpenProject/AddPreviewForViewComponent:
Include:
- app/components/op_turbo/**.rb
- app/components/op_primer/**.rb
- app/components/open_project/**.rb
- app/components/concerns/**.rb
+3 -3
View File
@@ -149,7 +149,7 @@ gem "structured_warnings", "~> 0.5.0"
# don't require by default, instead load on-demand when actually configured
gem "airbrake", "~> 13.0.0", require: false
gem "markly", "~> 0.14" # another markdown parser like commonmarker, but with AST support used in PDF export
gem "markly", "~> 0.15" # another markdown parser like commonmarker, but with AST support used in PDF export
gem "md_to_pdf", git: "https://github.com/opf/md-to-pdf", ref: "6c565541bfa390c58d90d49aa9b487777704fc66"
gem "prawn", "~> 2.4"
gem "ttfunk", "~> 1.7.0" # remove after https://github.com/prawnpdf/prawn/issues/1346 resolved.
@@ -198,7 +198,7 @@ gem "aws-sdk-s3", "~> 1.205"
gem "openproject-token", "~> 8.2.0"
gem "plaintext", "~> 0.3.2"
gem "plaintext", "~> 0.3.7"
gem "ruby-progressbar", "~> 1.13.0", require: false
@@ -356,7 +356,7 @@ group :development, :test do
gem "rubocop-factory_bot", require: false
gem "rubocop-openproject", require: false
gem "rubocop-performance", require: false
gem "rubocop-rails", "= 2.33.3", require: false # 2.33.4 has issues with Rails/ActionControllerFlashBeforeRender
gem "rubocop-rails", "2.34.2", require: false # 2.33.4 has issues with Rails/ActionControllerFlashBeforeRender
gem "rubocop-rspec", require: false
gem "rubocop-rspec_rails", require: false
+19 -19
View File
@@ -36,10 +36,10 @@ GIT
GIT
remote: https://github.com/opf/omniauth-openid-connect.git
revision: f0c1ecdb26e39017a9e929af75a166c772d960bb
ref: f0c1ecdb26e39017a9e929af75a166c772d960bb
revision: 825d06235b64f6bc872bba709f1c2d48fd5cede4
ref: 825d06235b64f6bc872bba709f1c2d48fd5cede4
specs:
omniauth-openid-connect (0.4.2)
omniauth-openid-connect (0.5.0)
addressable (~> 2.5)
omniauth (~> 1.6)
openid_connect (~> 2.2.0)
@@ -435,7 +435,7 @@ GEM
compare-xml (0.66)
nokogiri (~> 1.8)
concurrent-ruby (1.3.5)
connection_pool (2.5.4)
connection_pool (2.5.5)
cookiejar (0.3.4)
cose (1.3.1)
cbor (~> 0.5.9)
@@ -661,7 +661,7 @@ GEM
rake (>= 13)
googleapis-common-protos-types (1.22.0)
google-protobuf (~> 4.26)
googleauth (1.15.1)
googleauth (1.16.0)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1)
@@ -801,7 +801,7 @@ GEM
net-pop
net-smtp
marcel (1.1.0)
markly (0.14.1)
markly (0.15.0)
matrix (0.4.3)
messagebird-rest (5.0.0)
jwt (< 4)
@@ -1070,7 +1070,7 @@ GEM
ostruct (0.6.3)
ox (2.14.23)
bigdecimal (>= 3.0)
pagy (43.1.5)
pagy (43.1.8)
json
yaml
paper_trail (17.0.0)
@@ -1098,7 +1098,7 @@ GEM
pg (1.6.2-x86_64-darwin)
pg (1.6.2-x86_64-linux)
pg (1.6.2-x86_64-linux-musl)
plaintext (0.3.6)
plaintext (0.3.7)
activesupport (> 2.2.1)
nokogiri (~> 1.10, >= 1.10.4)
rubyzip (>= 1.2.0)
@@ -1333,7 +1333,7 @@ GEM
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
rubocop-rails (2.33.3)
rubocop-rails (2.34.2)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
@@ -1625,7 +1625,7 @@ DEPENDENCIES
lograge (~> 0.14.0)
lookbook (= 2.3.13)
mail (= 2.9.0)
markly (~> 0.14)
markly (~> 0.15)
matrix (~> 0.4.3)
md_to_pdf!
meta-tags (~> 2.22.2)
@@ -1676,7 +1676,7 @@ DEPENDENCIES
parallel_tests (~> 4.0)
pdf-inspector (~> 1.2)
pg (~> 1.6.2)
plaintext (~> 0.3.2)
plaintext (~> 0.3.7)
prawn (~> 2.4)
pry-byebug (~> 3.11.0)
pry-rails (~> 0.3.6)
@@ -1712,7 +1712,7 @@ DEPENDENCIES
rubocop-factory_bot
rubocop-openproject
rubocop-performance
rubocop-rails (= 2.33.3)
rubocop-rails (= 2.34.2)
rubocop-rspec
rubocop-rspec_rails
ruby-duration (~> 3.2.0)
@@ -1835,7 +1835,7 @@ CHECKSUMS
commonmarker (2.5.0-x86_64-linux-musl) sha256=3ac20d8de38260d02306e3a539b90d136818998bcfb203114f7054ad7fdb7b37
compare-xml (0.66) sha256=e21aa5c0f69ef1177eced997c688fd4df989084e74a1b612257af32e1dd05319
concurrent-ruby (1.3.5) sha256=813b3e37aca6df2a21a3b9f1d497f8cbab24a2b94cab325bffe65ee0f6cbebc6
connection_pool (2.5.4) sha256=e9e1922327416091f3f6542f5f4446c2a20745276b9aa796dd0bb2fd0ea1e70a
connection_pool (2.5.5) sha256=e54ff92855753df1fd7c59fa04a398833355f27dd14c074f8c83a05f72a716ad
cookiejar (0.3.4) sha256=11b16acfc4baf7a0f463c21a6212005e04e25f5554d4d9f24d97f3492dfda0df
cose (1.3.1) sha256=d5d4dbcd6b035d513edc4e1ab9bc10e9ce13b4011c96e3d1b8fe5e6413fd6de5
costs (1.0.0)
@@ -1928,7 +1928,7 @@ CHECKSUMS
google-protobuf (4.32.1-x86_64-linux-gnu) sha256=2d209c1980dbdeb4114c7d839a3305fb78dc90bde42fb9a22974c8a4841e0263
google-protobuf (4.32.1-x86_64-linux-musl) sha256=d5314aea8817bd372177205667b4f4442a5884212bcaccb1ba569ec8a1a06ec4
googleapis-common-protos-types (1.22.0) sha256=f97492b77bd6da0018c860d5004f512fe7cd165554d7019a8f4df6a56fbfc4c7
googleauth (1.15.1) sha256=d61960893d0d573601d94a38889613b991f40a59ab755b5beed87bf2c3d3cb24
googleauth (1.16.0) sha256=1e7b5c2ee7edc6a0f5a4a4312c579b3822dc0be2679d6d09ca19d8c7ca5bd5f1
grape (2.4.0) sha256=3d59673e80f11d49ba86270b78344e5348dc057b318c2bbc1c01f3532f9b6aec
grape_logging (3.0.0) sha256=7b62d984ce96df15d120508668debe307e6a59ac1c511f1d9b5f3b4bea793e13
gravatar_image_tag (1.2.0) sha256=eb5630fea846b711e713b934a0178fb9785f02f4eb9ced8d6faa4d537c40fdcf
@@ -1978,7 +1978,7 @@ CHECKSUMS
lookbook (2.3.13) sha256=acfa04a1ba7a87b057c222d78a2d72763546f52549e97590993344c8373f6d21
mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941
marcel (1.1.0) sha256=fdcfcfa33cc52e93c4308d40e4090a5d4ea279e160a7f6af988260fa970e0bee
markly (0.14.1) sha256=911970dcf1d077461ab39d46a6e7b68f3e8acbb3a7a9fec41393bad46dceab4f
markly (0.15.0) sha256=63328dac2f7c14d8fd4b9f9ed4c50f02f63a2b3e43ddeb8516aae528086f47ec
matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b
md_to_pdf (0.2.5)
messagebird-rest (5.0.0) sha256=da4cc1efba3d5e4aa021fad07426c2cb6b326ce5670da5104bb8f6056a39d59c
@@ -2014,7 +2014,7 @@ CHECKSUMS
oj (3.16.12) sha256=ad9fad6a06dabcf4cfe6a420690a4375377685c16eee0ae88e8d38a43ed7b556
okcomputer (1.19.0) sha256=8548935a82f725bdd8f2c329925a9f1a1bb2ce19ce26b47d7515665ee363b458
omniauth (1.9.2)
omniauth-openid-connect (0.4.2)
omniauth-openid-connect (0.5.0)
omniauth-openid_connect-providers (0.2.0)
omniauth-saml (1.10.6) sha256=13dde22f4fd1beff0ef2d6dae576f7b68594f159990e8e886d8a02b32397afbd
op-clamav-client (3.4.2) sha256=f28d697d11758a2ba3dc530cfdf4871a00ecd517631e8bac30dee30cd6012964
@@ -2108,7 +2108,7 @@ CHECKSUMS
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
overviews (1.0.0)
ox (2.14.23) sha256=4a9aedb4d6c78c5ebac1d7287dc7cc6808e14a8831d7adb727438f6a1b461b66
pagy (43.1.5) sha256=b4b8a12a6edd6d66fa8501989903238a5f767605ca306a90745c82b00f8cb543
pagy (43.1.8) sha256=06d89d0924ef74d239e2410f5e7baedcd73ebcd0a020bbe582af71c79dd9e38a
paper_trail (17.0.0) sha256=1c2842061d3874ca7015908e821e2aa14f9b982af2acb2a7974713bf79021c85
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
parallel_tests (4.10.1) sha256=df05458c691462b210f7a41fc2651d4e4e8a881e8190e6d1e122c92c07735d70
@@ -2123,7 +2123,7 @@ CHECKSUMS
pg (1.6.2-x86_64-darwin) sha256=c441a55723584e2ae41749bf26024d7ffdfe1841b442308ed50cd6b7fda04115
pg (1.6.2-x86_64-linux) sha256=525f438137f2d1411a1ebcc4208ec35cb526b5a3b285a629355c73208506a8ea
pg (1.6.2-x86_64-linux-musl) sha256=e5c8668ffeaf7a9c3458a3dcb002dffa6d8ee1fca9ae534ffef861d2b15644ca
plaintext (0.3.6) sha256=6ae13dce9f94e26bde4069109455a1d4948ae161e89350db1a9b37667b61f385
plaintext (0.3.7) sha256=3b923152ee84d98898d459e9442ec9cca84d2acdbb8d232e2bcf41c5723e70d5
pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
prawn (2.4.0) sha256=82062744f7126c2d77501da253a154271790254dfa8c309b8e52e79bc5de2abd
prawn-table (0.2.2) sha256=336d46e39e003f77bf973337a958af6a68300b941c85cb22288872dc2b36addb
@@ -2202,7 +2202,7 @@ CHECKSUMS
rubocop-factory_bot (2.28.0) sha256=4b17fc02124444173317e131759d195b0d762844a71a29fe8139c1105d92f0cb
rubocop-openproject (0.3.0) sha256=9554496e7ef0a2cf65dc2b32bee1bfa223b4f9ae058a5c603489d34e9001a828
rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
rubocop-rails (2.33.3) sha256=848c011b58c1292f3066246c9eb18abf6ffcfbce28bc57c4ab888bbec79af74b
rubocop-rails (2.34.2) sha256=10ff246ee48b25ffeabddc5fee86d159d690bb3c7b9105755a9c7508a11d6e22
rubocop-rspec (3.8.0) sha256=28440dccb3f223a9938ca1f946bd3438275b8c6c156dab909e2cb8bc424cab33
rubocop-rspec_rails (2.32.0) sha256=4a0d641c72f6ebb957534f539d9d0a62c47abd8ce0d0aeee1ef4701e892a9100
ruby-duration (3.2.3) sha256=eb3d13b1df85067a015a8fb2ed8f1eec842a3b721e47c9b6fd74d2f356069784
+1 -1
View File
@@ -8,7 +8,7 @@ gem 'omniauth-openid_connect-providers',
gem 'omniauth-openid-connect',
git: 'https://github.com/opf/omniauth-openid-connect.git',
ref: 'f0c1ecdb26e39017a9e929af75a166c772d960bb'
ref: '825d06235b64f6bc872bba709f1c2d48fd5cede4'
group :opf_plugins do
# included so that engines can reference OpenProject::Version
+2 -1
View File
@@ -38,7 +38,8 @@ module Users
}
attribute :firstname
attribute :lastname
attribute :mail
attribute :mail,
writable: ->(*) { model.new_record? || model.id == user.id || user.admin? }
attribute :admin,
writable: ->(*) { user.admin? && model.id != user.id }
attribute :language
+5 -1
View File
@@ -37,6 +37,7 @@ class LoginForm < ApplicationForm
f.text_field(
name: :username,
id: "username#{@id_suffix}",
value: @username,
autofocus: @username.blank?,
label: User.human_attribute_name(:login),
@@ -46,6 +47,7 @@ class LoginForm < ApplicationForm
f.text_field(
name: :password,
id: "password#{@id_suffix}",
type: :password,
autofocus: @username.present?,
label: User.human_attribute_name(:password),
@@ -55,6 +57,7 @@ class LoginForm < ApplicationForm
if Setting::Autologin.enabled?
f.check_box name: "autologin",
id: "autologin#{@id_suffix}",
checked: false,
value: 1,
label: I18n.t("users.autologins.prompt",
@@ -85,9 +88,10 @@ class LoginForm < ApplicationForm
end
end
def initialize(back_url: nil, username: nil)
def initialize(back_url: nil, username: nil, id_suffix: nil)
super()
@back_url = back_url
@username = username
@id_suffix = id_suffix
end
end
+8 -1
View File
@@ -33,7 +33,14 @@ See COPYRIGHT and LICENSE files for more details.
url: { controller: "/account", action: "login" },
method: :post
) do |form_builder|
render(LoginForm.new(form_builder, back_url: back_url_to_current_page, username: params[:username]))
render(
LoginForm.new(
form_builder,
back_url: back_url_to_current_page,
username: params[:username],
id_suffix: "-pulldown"
)
)
end
%>
@@ -22,6 +22,7 @@
<div class="form--field -required">
<%= f.text_field :mail,
required: true,
disabled: !(@user.new_record? || User.current.admin?),
container_class: "-middle" %>
</div>
+25 -25
View File
@@ -1227,7 +1227,7 @@ cs:
enabled_modules: "Povolené moduly"
identifier: "Identifikátor"
latest_activity_at: "Poslední aktivita"
parent: "Nadřazený projekt"
parent: "Podprojekt"
project_creation_wizard_enabled: "Project initiation request"
public_value:
title: "Viditelnost"
@@ -1593,7 +1593,7 @@ cs:
meeting:
error_conflict: "Nelze uložit, protože schůzku mezitím aktualizoval někdo jiný. Znovu načtěte stránku."
notifications:
at_least_one_channel: "Pro odesílání notifikací musí být specifikován alespoň jeden kanál"
at_least_one_channel: "Alespoň jeden kanál pro odesílání oznámení musí být specifikován."
attributes:
read_ian:
read_on_creation: "nelze nastavit na pravdivé při vytváření oznámení "
@@ -1891,11 +1891,11 @@ cs:
member: "Člen"
news: "Novinky"
notification:
one: "Notifikace"
few: "Notifikací"
many: "Notifikací"
other: "Notifikace"
placeholder_user: "Placeholder uživatel"
one: "Oznámení"
few: "Oznámení"
many: "Oznámení"
other: "Oznámení"
placeholder_user: "placeholder uživatel"
project:
one: "Projekt"
few: "Projekty"
@@ -2943,7 +2943,7 @@ cs:
instructions_after_error: "Zkuste se znovu přihlásit kliknutím na %{signin}. Pokud chyba přetrvává, požádejte správce o pomoc."
menus:
admin:
mail_notification: "E-mailové notifikace"
mail_notification: "E-mailová upozornění"
mails_and_notifications: "E-maily a oznámení"
aggregation: "Agregace"
api_and_webhooks: "API & Webhooky"
@@ -3007,7 +3007,7 @@ cs:
by_project: "Nepřečteno dle projektu"
by_reason: "Důvod"
inbox: "Doručená pošta"
send_notifications: "Pro tuto akci odeslat notifikaci"
send_notifications: "Odeslat oznámení pro tuto akci"
work_packages:
subject:
created: "Pracovní balíček byl vytvořen."
@@ -3447,9 +3447,9 @@ cs:
label_permissions: "Práva"
label_permissions_report: "Přehled oprávnění"
label_personalize_page: "Přizpůsobit tuto stránku"
label_placeholder_user: "Placeholder uživatel"
label_placeholder_user: "placeholder uživatel"
label_placeholder_user_new: ""
label_placeholder_user_plural: "Placeholder uživatelé"
label_placeholder_user_plural: "placeholder uživatelé"
label_planning: "Plánování"
label_please_login: "Přihlaste se prosím"
label_plugins: "Pluginy"
@@ -3473,7 +3473,7 @@ cs:
label_project_attribute_plural: "Atributy projektu"
label_project_attribute_manage_link: "Správa atributů produktu"
label_project_count: "Celkový počet projektů"
label_project_copy_notifications: "Během kopírování projektu odeslat notifikace e-mailem"
label_project_copy_notifications: "Během kopie projektu odeslat oznámení e-mailem"
label_project_initiation_export_pdf: "Export PDF for %{project_creation_name}"
label_project_latest: "Nejnovější projekty"
label_project_default_type: "Povolit prázdný typ"
@@ -3641,7 +3641,7 @@ cs:
label_version_new: "Nová verze"
label_version_edit: "Upravit verzi"
label_version_plural: "Verze"
label_version_sharing_descendants: "S podprojekty"
label_version_sharing_descendants: "S Podprojekty"
label_version_sharing_hierarchy: "S hierarchií projektu"
label_version_sharing_none: "Není sdíleno"
label_version_sharing_system: "Se všemi projekty"
@@ -3749,28 +3749,28 @@ cs:
digests:
including_mention_singular: "včetně zmínky"
including_mention_plural: "včetně %{number_mentioned} zmínění"
unread_notification_singular: "1 nepřečtená notifikace"
unread_notification_plural: "%{number_unread} nepřečtených notifikací"
unread_notification_singular: "1 nepřečtené oznámení"
unread_notification_plural: "%{number_unread} nepřečtených oznámení"
you_have: "Máte"
logo_alt_text: "Logo"
mention:
subject: "%{user_name} vás zmínil v #%{id} - %{subject}"
notification:
center: "Centrum notifikací"
center: "Centrum oznámení"
see_in_center: "Zobrazit komentář v oznamovacím centru"
settings: "Změnit nastavení e-mailu"
salutation: "Ahoj %{user}!"
salutation_full_name: "Jméno a příjmení"
work_packages:
created_at: "Vytvořeno v %{timestamp} uživatelem %{user} "
login_to_see_all: "Přihlaste se pro zobrazení všech notifikací."
login_to_see_all: "Přihlaste se pro zobrazení všech oznámení."
mentioned: "Byli jste <b>zmíněni v komentáři</b>"
mentioned_by: "%{user} vás zmínil v komentáři OpenProject"
more_to_see:
one: "Existuje ještě 1 pracovní balíček s notifikací."
few: "Existuje ještě %{count} pracovních balíčků s notifikacema."
many: "Existuje ještě %{count} pracovních balíčků s notifikacema."
other: "Existuje ještě %{count} pracovních balíčků s notifikacema."
one: "Máte ještě 1 pracovní balíček s notifikací."
few: "Existuje ještě %{count} pracovních balíčků s oznámeními."
many: "Máte ještě %{count} pracovních balíčků s notifikacemi."
other: "Existuje ještě %{count} pracovních balíčků s oznámeními."
open_in_browser: "Otevřít v prohlížeči"
reason:
watched: "Sledováno"
@@ -3779,7 +3779,7 @@ cs:
mentioned: "Zmíněné"
shared: "Sdílené"
subscribed: "vše"
prefix: "Obdrženo z důvodu nastavení notifikací: %{reason}"
prefix: "Obdrženo z důvodu nastavení oznámení: %{reason}"
date_alert_start_date: "Upozornění na datum"
date_alert_due_date: "Upozornění na datum"
reminder: "Připomínka"
@@ -4080,7 +4080,7 @@ cs:
permission_move_work_packages: "Přesun pracovních balíčků"
permission_protect_wiki_pages: "Ochrana stránky wiki"
permission_rename_wiki_pages: "Přejmenovat stránky wiki"
permission_save_queries: "Uložit zobrazení"
permission_save_queries: "Uložit pohled"
permission_search_project: "Hledat projekt"
permission_select_custom_fields: "Vybrat vlastní pole"
permission_select_project_custom_fields: "Vyberte atributy projektu"
@@ -4550,7 +4550,7 @@ cs:
enable_subscriptions_text_html: Umožňuje uživatelům s nezbytnými oprávněními přihlásit se do OpenProject kalendářů a získat přístup k informacím o pracovním balíčku prostřednictvím externího klienta kalendáře. <strong>Poznámka:</strong> Před povolením si prosím přečtěte <a href="%{link}" target="_blank">podrobnosti o odběru</a>.
language_name_being_default: "%{language_name} (výchozí)"
notifications:
events_explanation: "Určuje, pro kterou událost je odeslán e-mail. Pracovní balíčky jsou z tohoto seznamu vyloučeny, protože notifikace pro ně mohou být nastavena speciálně pro každého uživatele."
events_explanation: "Určuje, pro kterou událost je odeslán e-mail. Pracovní balíčky jsou z tohoto seznamu vyloučeny, protože oznámení pro ně mohou být nastavena speciálně pro každého uživatele."
delay_minutes_explanation: "Odesílání e-mailu může být pozdrženo, aby bylo uživatelům s nakonfigurovaným v oznámení aplikace před odesláním pošty potvrzeno oznámení. Uživatelé, kteří si přečtou oznámení v aplikaci, nedostanou e-mail pro již přečtené oznámení."
other: "Ostatní"
passwords: "Hesla"
@@ -4727,7 +4727,7 @@ cs:
text_destroy_with_associated: "Existují další objekty, které jsou přiřazeny k pracovním balíčkům a které mají být odstraněny. Tyto objekty jsou následující typy:"
text_destroy_what_to_do: "Co chcete udělat?"
text_diff_truncated: "... Toto rozlišení bylo zkráceno, protože přesahuje maximální velikost, kterou lze zobrazit."
text_email_delivery_not_configured: "Doručení e-mailu není nakonfigurováno a notifikace jsou zakázány.\nNakonfigurujte váš SMTP server pro jejich povolení."
text_email_delivery_not_configured: "Doručení e-mailu není nakonfigurováno a oznámení jsou zakázána.\nNakonfigurujte váš SMTP server pro jejich povolení."
text_enumeration_category_reassign_to: "Přiřadit je k této hodnotě:"
text_enumeration_destroy_question: "%{count} objektů je přiřazeno k této hodnotě."
text_file_repository_writable: "Do adresáře příloh lze zapisovat"
+14 -14
View File
@@ -85,11 +85,11 @@ de:
title: "Enterprise-Token hinzufügen"
type_token_text: "Enterprise-Token Text"
token_placeholder: "Enterprise-Token Text hier einfügen"
add_token: "Enterprise-Edition Support Token hochladen"
add_token: "Enterprise edition Support Token hochladen"
replace_token: "Aktuellen Enterprise edition Support Token ersetzen"
order: "Enterprise on-premises bestellen"
paste: "Enterprise-Edition Support Token hier einfügen"
required_for_feature: "Dieses Add-on ist nur mit einem aktiven Enterprise-Edition Support-Token verfügbar."
paste: "Enterprise edition Support Token hier einfügen"
required_for_feature: "Dieses Add-on ist nur mit einem aktiven Enterprise edition Support-Token verfügbar."
enterprise_link: "Klicken Sie hier für weitere Informationen."
start_trial: "Kostenlose Testversion starten"
book_now: "Jetzt buchen"
@@ -866,10 +866,10 @@ de:
tab: "Titel konfigurieren"
manually_editable_subjects:
label: "Manuell bearbeitbare Titel"
caption: "Nutzer:innen können die Titel der Arbeitspakete ohne Einschränkungen manuell eingeben und bearbeiten."
caption: "Benutzer können die Titel der Arbeitspakete ohne Einschränkungen manuell eingeben und bearbeiten."
automatically_generated_subjects:
label: "Automatisch generierte Titel"
caption: "Definieren Sie ein Schema aus referenzierten Attributen und Freitext für die automatische Generierung von Arbeitspakettiteln. Nutzer:innen können diese nicht manuell editieren."
caption: "Definieren Sie ein Schema aus referenzierten Attributen und Freitext für die automatische Generierung von Arbeitspakettiteln. Nutzer können diese nicht manuell editieren."
token:
label_with_context: "%{attribute_context}: %{attribute_label}"
context:
@@ -913,7 +913,7 @@ de:
manual_with_children: "Hat Unteraufgaben aber ihre Startdaten werden ignoriert."
title:
automatic_mobile: "Automatisch geplant."
automatic_with_children: "Unteraufgaben bestimmen Termine."
automatic_with_children: "Die Termine sind durch untergeordnete Arbeitspakete bestimmt."
automatic_with_predecessor: "Der Anfangstermin wird von einem Vorgänger festgelegt."
manual_mobile: "Manuell geplant."
manually_scheduled: "Manuell geplant Daten unabhängig von Beziehungen."
@@ -1016,7 +1016,7 @@ de:
label_child_plural: "Unteraufgaben"
new_child: "Neue Unteraufgabe"
new_child_description: "Erstellt ein zugehöriges Arbeitspaket als Unteraufgabe des aktuellen (übergeordneten) Arbeitspakets"
child: "Unteraufgabe"
child: "Kind"
child_description: "Macht das zugehörige Arbeitspaket zu einer Unteraufgabe des aktuellen (übergeordneten) Arbeitspakets"
parent: "Übergeordnetes Arbeitspaket"
parent_description: "Wandelt das verknüpfte in ein übergeordnetes Arbeitspaket dieses Arbeitspakets um"
@@ -1257,7 +1257,7 @@ de:
column_names: "Spalten"
relations_to_type_column: "Beziehungen zu %{type}"
relations_of_type_column: "Beziehungen der Art: %{type}"
child_work_packages: "Unteraufgaben"
child_work_packages: "Kinder"
group_by: "Gruppiere Ergebnisse nach"
sort_by: "Ergebnisse sortieren nach"
filters: "Filter"
@@ -1750,7 +1750,7 @@ de:
status_transition_invalid: "ist ungültig, da kein valider Übergang vom alten zum neuen Status für die aktuelle Rolle des Nutzers existiert."
status_invalid_in_type: "ist ungültig, da der aktuelle Status nicht in diesem Typ vorhanden ist."
type:
cannot_be_milestone_due_to_children: "kann kein Meilenstein werden, da dieses Arbeitspaket Unteraufgaben besitzt."
cannot_be_milestone_due_to_children: "kann kein Meilenstein werden, da dieses Arbeitspaket Unterelemente besitzt."
priority_id:
only_active_priorities_allowed: "muss aktiv sein."
category:
@@ -2541,7 +2541,7 @@ de:
error_custom_option_not_found: "Option ist nicht vorhanden."
error_enterprise_plan_needed: "Sie benötigen den Enterprise-Plan %{plan}, um diese Aktion durchzuführen."
error_enterprise_activation_user_limit: "Ihr Konto konnte nicht aktiviert werden (Nutzerlimit erreicht). Bitte kontaktieren Sie Ihren Administrator um Zugriff zu erhalten."
error_enterprise_token_invalid_domain: "Die Enterprise-Edition ist nicht aktiv. Die aktuelle Domain (%{actual}) entspricht nicht dem erwarteten Hostnamen (%{expected})."
error_enterprise_token_invalid_domain: "Die Enterprise edition ist nicht aktiv. Die aktuelle Domain (%{actual}) entspricht nicht dem erwarteten Hostnamen (%{expected})."
error_failed_to_delete_entry: "Fehler beim Löschen dieses Eintrags."
error_in_dependent: "Fehler beim Versuch, abhängiges Objekt zu ändern: %{dependent_class} #%{related_id} - %{related_subject}: %{error}"
error_in_new_dependent: "Fehler beim Versuch, abhängiges Objekt zu erstellen: %{dependent_class} - %{related_subject}: %{error}"
@@ -2814,7 +2814,7 @@ de:
dates:
working: "%{date} ist jetzt ein Arbeitstag"
non_working: "%{date} ist jetzt ein arbeitsfreier Tag"
progress_mode_changed_to_status_based: Fortschrittberechnung wurde auf Status-bezogen gesetzt
progress_mode_changed_to_status_based: Fortschrittberechnung wurde auf Status-basiert gesetzt
status_excluded_from_totals_set_to_false_message: jetzt in den Gesamtwerten der Hierarchie enthalten
status_excluded_from_totals_set_to_true_message: jetzt von den Hierarchie-Gesamtwerten ausgeschlossen
status_percent_complete_changed: "% abgeschlossen von %{old_value}% auf %{new_value} % geändert"
@@ -3111,7 +3111,7 @@ de:
label_enumerations: "Aufzählungen"
label_enterprise: "Enterprise"
label_enterprise_active_users: "%{current}/%{limit} gebuchte aktive Nutzer"
label_enterprise_edition: "Enterprise Edition"
label_enterprise_edition: "Enterprise edition"
label_enterprise_support: "Enterprise Support"
label_environment: "Umgebung"
label_estimates_and_progress: "Schätzungen und Fortschritt"
@@ -4171,7 +4171,7 @@ de:
update_timeout: "Speichere die Informationen bzgl. des genutzten Festplattenspeichers eines Projektarchivs für N Minuten.\nErhöhen Sie diesen Wert zur Verbesserung der Performance, da die Erfassung des genutzten Festplattenspeichers Ressourcen-intensiv ist."
oauth_application_details: "Der Client Geheimcode wird nach dem Schließen dieses Fensters nicht mehr zugänglich sein. Bitte kopieren Sie diese Werte in die Nextcloud OpenProject Integrationseinstellungen:"
oauth_application_details_link_text: "Zu den Einstellungen gehen"
setup_documentation_details: "Wenn Sie Hilfe bei der Konfiguration eines neuen Dateispeichers benötigen, konsultieren Sie bitte die Dokumentation: "
setup_documentation_details: "Wenn Sie Hilfe bei der Konfiguration eines neuen Datei-Speichers benötigen, konsultieren Sie bitte die Dokumentation: "
setup_documentation_details_link_text: "Dateispeicher einrichten"
show_warning_details: "Um diesen Dateispeicher nutzen zu können, müssen Sie das Modul und den spezifischen Speicher in den Projekteinstellungen jedes gewünschten Projekts aktivieren."
subversion:
@@ -4791,7 +4791,7 @@ de:
warning_user_limit_reached_admin: >
Das Hinzufügen zusätzlicher Benutzer überschreitet das aktuelle Benutzerlimit. Bitte <a href="%{upgrade_url}">aktualisieren Sie Ihr Abonnement</a> um sicherzustellen, dass externe Benutzer auf diese Instanz zugreifen können.
warning_user_limit_reached_instructions: >
Du hast dein Nutzerlimit erreicht (%{current}/%{max} active users). Bitte kontaktiere sales@openproject.com um deinen Enterprise Edition Plan upzugraden und weitere Nutzer hinzuzufügen.
Du hast dein Nutzerlimit erreicht (%{current}/%{max} active users). Bitte kontaktiere sales@openproject.com um deinen Enterprise edition Plan upzugraden und weitere Nutzer hinzuzufügen.
warning_protocol_mismatch_html: >
warning_bar:
+1 -1
View File
@@ -921,7 +921,7 @@ es:
automatic_with_children: "Fechas determinadas por paquetes de trabajo secundarios."
automatic_with_predecessor: "La fecha de inicio la fija un predecesor."
manual_mobile: "Programado manualmente."
manually_scheduled: "Programado manualmente. No afectadas por relaciones."
manually_scheduled: "Programado manualmente. Fechas no afectadas por relaciones."
blankslate:
title: "Sin predecesores"
description: "Para activar la programación automática, este paquete de trabajo debe tener al menos un predecesor. Entonces se programará automáticamente para que comience después del predecesor más cercano."
+54 -53
View File
@@ -107,17 +107,17 @@ ja:
jemalloc_allocator: Jemalloc メモリアロケータ
journal_aggregation:
explanation:
text: "ユーザーの個々のアクション(例えば、ワークパッケージを2回更新するは、それらの年齢差が指定されたタイムスパン未満である場合、単一のアクションに集約されます。これらはアプリケーション内で1つのアクションとして表示されます。これはまた、送信されるメールの数を減らし、 %{webhook_link} の遅延にも影響します。"
text: "ユーザーの個々のアクション (例:ワークパッケージを2回更新する)は、指定された時間範囲よりも時間差が小さい場合、単一のアクションに集約されます。 これらはアプリケーション内で単一のアクションとして表示されます。 これにより、送信されるメールの数が減少し、 %{webhook_link} の遅延にも影響します。"
link: "webhook"
scim_clients:
authentication_methods:
sso: "IDプロバイダからのJWT"
oauth2_client: "OAuth 2.0クライアント認証情報"
sso: "アイデンティティプロバイダからのJWT"
oauth2_client: "OAuth 2.0 クライアント資格情報"
oauth2_token: "静的アクセストークン"
created_client_credentials_dialog_component:
title: "クライアント認証情報作成"
heading: "クライアント認証情報が生成されました"
one_time_hint: "クライアントシークレットが表示されるのはこの時だけです。必ずコピーしてください。"
title: "クライアントの資格情報作成されました"
heading: "クライアントの資格情報が生成されました"
one_time_hint: "クライアントシークレットが表示される唯一の時間です。今すぐコピーしてください。"
created_token_dialog_component:
title: "トークンを作成しました"
heading: "トークンが生成されました"
@@ -130,21 +130,21 @@ ja:
edit:
label_delete_scim_client: "SCIM クライアントを削除"
form:
auth_provider_description: "これは、SCIM プロバイダによって追加されたユーザが OpenProject で認証するために使用するサービスです。"
authentication_method_description_html: "これは SCIM クライアントが OpenProject で認証する方法です。OAuth トークンに<code>scim_v2</code>スコープが含まれていることを確認してください。"
description: "これらの設定オプションの詳細については、[SCIMクライアントの設定に関する文書](docs_url)を参照してください。"
auth_provider_description: "これは、SCIMプロバイダが追加したユーザがOpenProjectでの認証に使用するサービスです。"
authentication_method_description_html: "これは SCIM クライアントが OpenProject で認証する方法です。OAuth トークンに <code>scim_v2</code> スコープが含まれていることを確認してください。"
description: "設定オプションの詳細については、[SCIM クライアントの設定に関するドキュメント](docs_url)を参照してください。"
jwt_sub_description: "例えば、Keycloakの場合、これはSCIMクライアントに関連付けられたサービスアカウントのUUIDです。あなたのユースケースにあった Subject claim を見つける方法については [ドキュメント](docs_url) を参照してください。"
name_description: "このクライアントが設定された理由を他の管理者が理解しやすい名前を選んでください。"
name_description: "他の管理者がこのクライアントが設定された理由を理解するのに役立つ名前を選択してください。"
index:
description: "ここで設定された SCIM クライアントは、OpenProject SCIM サーバ API と対話し、ユーザアカウントグループのプロビジョニング、更新、デプロビジョニングを行うことができます。"
label_create_button: "SCIMクライアント追加"
description: "ここで設定されたSCIMクライアントは、OpenProjectSCIMサーバ APIと相互作用して、ユーザアカウントグループのプロビジョニング、更新、およびデプロビジョニングを行うことができます。"
label_create_button: "SCIMクライアント追加"
new:
title: "新しいSCIMクライアント"
revoke_static_token_dialog_component:
confirm_button: "取り消す"
title: "静的トークンの失効"
heading: "このトークンを本当に取り消しすか?"
description: "このトークンを使っている SCIM クライアントは、OpenProject の SCIM サーバ API にアクセスできなくなります。"
title: "静的トークンを取り消す"
heading: "このトークンを取り消してもよろしいですか?"
description: "このトークンを使用する SCIM クライアントは、OpenProject の SCIM サーバ API にアクセスできなくなります。"
table_component:
blank_slate:
title: "SCIMクライアントがまだ設定されていません"
@@ -657,26 +657,26 @@ ja:
other: "また、 %{shared_work_packages_link} はこのユーザーと共有されています。"
remove_project_membership_or_work_package_shares_too: "直接のメンバーとしてのユーザーだけを削除したい(および共有を維持したい)、またはワークパッケージの共有も削除しますか?"
will_remove_all_user_access_priveleges: "このメンバーを削除すると、プロジェクトへのユーザーのすべてのアクセス権が削除されます。ユーザーはまだサイトの一部として存在します。"
will_remove_all_group_access_priveleges: "このメンバを削除すると、プロジェクトに対するグループのすべてのアクセス権が削除されます。グループはサイトの一部としてまだ存在します。"
cannot_delete_inherited_membership: "このプロジェクトのメンバーであるグループに属しているため、このメンバーを削除することはできません。"
cannot_delete_inherited_membership_note_admin_html: "%{administration_settings_link}で、プロジェクトのメンバーとしてグループを削除することも、特定のメンバーをグループから削除することできます。"
cannot_delete_inherited_membership_note_non_admin: "プロジェクトのメンバーとしてグループを削除するか、管理者に連絡してこの特定のメンバーをグループから削除することができます。"
will_remove_all_group_access_priveleges: "このメンバを削除すると、グループのすべてのアクセス権がプロジェクトに削除されます。グループはサイトの一部として存在します。"
cannot_delete_inherited_membership: "このメンバーはこのプロジェクトのメンバーであるグループに属しているため、削除できません。"
cannot_delete_inherited_membership_note_admin_html: "プロジェクトのメンバーとしてグループを削除するか、 %{administration_settings_link} のグループからこの特定のメンバーを削除することできます。"
cannot_delete_inherited_membership_note_non_admin: "プロジェクトのメンバーとしてグループを削除するか、管理者に問い合わせてグループから特定のメンバーを削除することができます。"
delete_work_package_shares_dialog:
title: "ワークパッケージ・シェアの破棄"
title: "ワークパッケージの共有の取り消し"
shared_with_this_user_html:
other: "%{all_shared_work_packages_link} はこのユーザーと共有されています。"
shared_with_this_group_html:
other: "%{all_shared_work_packages_link} はこのグループと共有されています。"
shared_with_permission_html:
other: "%{shared_work_packages_link} のみが %{shared_role_name} 権限と共有されています。"
revoke_all_or_with_role: "すべての共有ワークパッケージ、または %{shared_role_name} 権限を持つワークパッケージのみへのアクセス権を剥奪しますか?"
will_not_affect_inherited_shares: "(これは、そのグループと共有ているワークパッケージには影響しません。"
cannot_remove_inherited: "グループで共有されワークパッケージの共有は削除できません。"
cannot_remove_inherited_with_role: "ロール %{shared_role_name} 共有されるワークパッケージは、グループを介して共有され、削除することはできません。"
cannot_remove_inherited_note_admin_html: "%{administration_settings_link}、グループへの共有を取り消すか、グループからこの特定のメンバーを削除することができます。"
cannot_remove_inherited_note_non_admin: "グループへの共有を取り消すか、管理者に連絡して特定のメンバーをグループから削除することができます。"
will_revoke_directly_granted_access: "このアクションは、グループと共有されているワークパッケージ以外の、すべてのワークパッケージへのアクセス権を剥奪する。"
will_revoke_access_to_all: "このアクションは、すべてのアクセス権を剥奪する。"
revoke_all_or_with_role: "共有されたワークパッケージ、または %{shared_role_name} 権限を持つワークパッケージのみへのアクセスを取り消しますか?"
will_not_affect_inherited_shares: "(これはグループと共有されているワークパッケージには影響しません)。"
cannot_remove_inherited: "グループで共有されワークパッケージは削除できません。"
cannot_remove_inherited_with_role: "ワークパッケージとロール %{shared_role_name} 共有されているため、削除できません。"
cannot_remove_inherited_note_admin_html: "あなたは、グループへの共有を取り消すか、 %{administration_settings_link}グループからこの特定のメンバーを削除することができます。"
cannot_remove_inherited_note_non_admin: "共有をグループ取り消すか、管理者に問い合わせてグループから特定のメンバーを削除することができます。"
will_revoke_directly_granted_access: "このアクションは、すべてのユーザーへのアクセスを取り消しますが、グループと共有されているワークパッケージです。"
will_revoke_access_to_all: "このアクションは、すべてのユーザーへのアクセスを取り消します。"
my:
access_token:
dialog:
@@ -698,14 +698,14 @@ ja:
no_results_title_text: "現在、有効なアクセス トークンはありません。"
notice_api_token_revoked: "APIトークンが削除されました。新しいトークンを作成するには、APIセクションの作成ボタンを使用してください。"
notice_rss_token_revoked: "RSSトークンが削除されました。新しいトークンを作成するには、RSSセクションのリンクを使用してください。"
notice_ical_token_revoked: 'プロジェクト "%{project_name}" のカレンダー "%{calendar_name}" の iCalendar トークン "%{token_name}" が失効しました。このトークンを持つiCalendar URLは無効になりました。'
notice_ical_token_revoked: 'プロジェクト "%{token_name}" のカレンダー "%{calendar_name}" の iCalendar トークン "%{project_name}" が取り消されました。 このトークンiCalendar URLは無効です。'
news:
index:
no_results_title_text: 現在、報告するニュースはありません。
no_results_content_text: ニュース項目を追加
users:
autologins:
prompt: "ログインしたまま %{num_days}"
prompt: "%{num_days} のログインを維持"
sessions:
session_name: "%{browser_name} %{browser_version} の %{os_name}"
browser: "ブラウザ"
@@ -719,17 +719,17 @@ ja:
current: "Current (this device)"
title: "セッション管理"
instructions: "You are logged in to your account through the following devices. Revoke sessions that you do not recognise or from devices you do not control."
may_not_delete_current: "現在のセッション削除することはできません。"
may_not_delete_current: "現在のセッション削除できません。"
deletion_warning: "Are you sure you want to revoke this session? You will be logged out on this device."
groups:
member_in_these_groups: "このユーザーは現在以下のグループのメンバーです:"
no_results_title_text: このユーザーは現在どのグループのメンバーでもありません。
summary_with_more: '%{names} と %{count_link}のメンバー。'
more: "%{count} もっと見る"
summary: '%{names}のメンバー'
summary_with_more: '%{names} と %{count_link} のメンバー。'
more: "%{count} 以上"
summary: '%{names} のメンバー .'
memberships:
no_results_title_text: このユーザは現在プロジェクトのメンバーではありません。
open_profile: "プロフィール"
open_profile: "プロファイルを開く"
invite_user_modal:
invite: "招待"
title:
@@ -786,7 +786,7 @@ ja:
placeholder_users:
right_to_manage_members_missing: >
プレースホルダーユーザを削除する権限がありません。 プレースホルダー ユーザーがメンバーであるすべてのプロジェクトのメンバーを管理する権利はありません。
delete_tooltip: "プレースホルダーユーザー削除"
delete_tooltip: "プレースホルダー ユーザー削除"
deletion_info:
heading: "プレースホルダー ユーザー %{name} を削除"
data_consequences: >
@@ -804,11 +804,11 @@ ja:
reactions:
action_title: "リアクト"
add_reaction: "リアクションを追加"
react_with: "%{reaction} と リアクト"
and_user: "および %{user}"
react_with: "%{reaction} で反応する"
and_user: " %{user}"
and_others:
other: と %{count} その他
reaction_by: "%{reaction} によって"
reaction_by: "%{reaction} によ"
reportings:
index:
no_results_title_text: 現在、ステータス報告はありません。
@@ -819,19 +819,20 @@ ja:
このステータスの色を割り当てたり変更する場合にクリックします。
ステータスボタンに表示され、テーブル内のワークパッケージを強調表示するために使用できます。
status_default_text: |-
新しいワークパッケージはデフォルトでこのタイプに設定され。読み取り専用にはできない
新しいワークパッケージはデフォルトでこのタイプに設定されています。読み取り専用にすることはできません
status_excluded_from_totals_text: |-
このステータスを持つワークパッケージを、階層内の「作業」、「
残作業」、「完了率」の合計から除外するには、このオプションをオンにします。
このオプションをオンにすると、このステータスワークパッケージを合計作業量
残作業量、および階層構造で完了させることができます。
status_percent_complete_text: |-
<a href="%{href}">ステータスベースの進捗計算モードでは</a>、このステータスが選択されると、作業
パッケージの「完了%」が自動的にこの値に設定される。
ワークベースモードでは無視される。
status_readonly_html: |
このステータスを持つワークパッケージを読み取り専用としてマークするには、このオプションをチェックする
ステータス以外の属性は変更できません。
ワークパッケージを読み取り専用としてマークするには、このオプションをオンにしてください
ステータスを除いて変更することはできません。
<br>
<strong>注意</strong>: 継承された値 (子やリレーションなど) 適用されます。
<strong>メモ</strong>: 継承された値 (例えば、子や関連) 適用されます。
index:
no_results_title_text: 現在、ワークパッケージのステータスはありません。
no_results_content_text: 新しいステータスを追加
@@ -841,7 +842,7 @@ ja:
is_readonly: "読み取り専用"
excluded_from_totals: "合計から除外"
themes:
dark: "暗い"
dark: "ダーク"
light: "ライト"
sync_with_os: "自動(OSのテーマ設定に追従)"
types:
@@ -949,15 +950,15 @@ ja:
could_not_be_saved: "次のワークパッケージを保存できませんでした:"
none_could_be_saved: "%{total} 作業パッケージのどれも更新できませんでした。"
x_out_of_y_could_be_saved: "%{failing} の %{total} ワークパッケージのうち、 %{success} を更新できませんでした。"
selected_because_descendants: "%{selected} ワークパッケージが選択されたが、合計 %{total} ワークパッケージが影響を受け、その中には子孫も含まれる。"
descendant: "選択された子孫"
selected_because_descendants: "%{selected} ワークパッケージが選択されている間、合計 %{total} ワークパッケージが子孫を含む影響を受けます。"
descendant: "選択された子孫です"
move:
no_common_statuses_exists: "選択されたすべてのワークパッケージに利用できるステータスはありません。 それらの状態は変更できません。"
unsupported_for_multiple_projects: "複数のプロジェクトからのワークパッケージの一括移動 / コピーはサポートされていません"
current_type_not_available_in_target_project: >
ワークパッケージの現在のタイプがターゲットプロジェクトで有効になっていません。変更しない場合は、ターゲットプロジェクトタイプを有効にしてください。そうでない場合は、リストからターゲットプロジェクトで使用可能なタイプを選択してください
ターゲット プロジェクトで現在のワークパッケージのタイプが有効になっていません。 変更を行わないようにしたい場合は、対象プロジェクトタイプを有効にしてください。 それ以外の場合は、リストからターゲット プロジェクトで使用可能なタイプを選択します
bulk_current_type_not_available_in_target_project: >
ワークパッケージの現在のタイプがターゲットプロジェクトで有効になっていません。変更しない場合は、ターゲットプロジェクトタイプを有効にしてください。そうでない場合は、リストからターゲットプロジェクトで使用可能なタイプを選択してください
現在のタイプのワークパッケージターゲット プロジェクトで有効になっていません。 変更を行わないようにしたい場合は、対象プロジェクトタイプを有効にしてください。 それ以外の場合は、リストからターゲット プロジェクトで使用可能なタイプを選択します
sharing:
missing_workflow_warning:
title: "ワークパッケージの共有のためのワークフローがありません"
@@ -982,9 +983,9 @@ ja:
no_results_title_text: 現在、有効なバージョンはありません。
work_package_relations_tab:
index:
action_bar_title: "他のワークパッケージとのリレーションを追加して、それらの間にリンクを作成す。"
no_results_title_text: 現在利用可能な関係はない
blankslate_heading: "関係なし"
action_bar_title: "他のワークパッケージリレーションを追加して、その間にリンクを作成します。"
no_results_title_text: 現在利用可能なリレーションはありません
blankslate_heading: "リレーションなし"
blankslate_description: "このワークパッケージにはまだリレーションがありません。"
label_add_child_button: "子要素"
label_add_x: "%{x} を追加"
+1 -1
View File
@@ -104,7 +104,7 @@ ca:
button_save: "Desa"
button_settings: "Configuració"
button_uncheck_all: "Desmarca-ho tot"
button_update: "Actualitzar"
button_update: "Actualitza"
button_export-atom: "Descarregar Atom"
button_generate_pdf: "Generate PDF"
button_create: "Crear"
+4 -4
View File
@@ -138,7 +138,7 @@ de:
description_available_columns: "Verfügbare Spalten"
description_current_position: "Sie sind hier: "
description_select_work_package: "Arbeitspaket #%{id} auswählen"
description_subwork_package: "Unteraufgabe von Arbeitspaket #%{id}"
description_subwork_package: "Kind von Arbeitspaket #%{id}"
editor:
revisions: "Lokale Änderungen anzeigen"
no_revisions: "Keine lokalen Änderungen gefunden"
@@ -452,7 +452,7 @@ de:
label_total_progress: "%{percent}% Gesamtfortschritt"
label_total_amount: "Gesamt: %{amount}"
label_updated_on: "aktualisiert am"
label_value_derived_from_children: "(aggregierter Wert von Unteraufgaben)"
label_value_derived_from_children: "(aggregierter Wert von Kindelementen)"
label_children_derived_duration: "Aggregierte Dauer der Unteraufgaben"
label_warning: "Warnung"
label_work_package: "Arbeitspaket"
@@ -864,7 +864,7 @@ de:
title: "Neues Arbeitspaket"
header: "Neu: %{type}"
header_no_type: "Neues Arbeitspaket (Typ noch nicht gesetzt)"
header_with_parent: "Neu: %{type} (Unteraufgabe von %{parent_type} #%{id})"
header_with_parent: "Neu: %{type} (Kind von %{parent_type} #%{id})"
button: "Erstellen"
duplicate:
title: "Arbeitspaket duplizieren"
@@ -1061,7 +1061,7 @@ de:
single_text: "Sind Sie sicher, dass Sie das Arbeitspaket löschen möchten?"
bulk_text: "Sind Sie sicher, dass Sie die folgenden %{label} löschen möchten?"
has_children: "Dieses Arbeitspaket hat %{childUnits}:"
confirm_deletion_children: "Ich bestätige, dass alle Unteraufgaben der hier aufgeführten Arbeitspakete rekursiv entfernt werden."
confirm_deletion_children: "Ich bestätige, dass alle untergordneten Elemente der hier aufgeführten Arbeitspakete rekursiv entfernt werden."
deletes_children: "Alle Unteraufgaben und deren Nachkommen werden auch rekursiv gelöscht."
destroy_time_entry:
title: "Löschen der Zeitbuchung bestätigen"
+41 -41
View File
@@ -32,12 +32,12 @@ ja:
draggable_hint: |
埋め込み画像または添付ファイルをエディタにドラッグします。
ドラッグしつづけると閉じているエディタ領域が開きます。
quarantined_hint: "ウイルスが発見されたように、ファイルは隔離されています。ダウンロードできません。"
quarantined_hint: "ウイルスが発見されたため,ファイルは隔離されています。ダウンロードできません。"
autocomplete_ng_select:
add_tag: "アイテムを追加"
add_tag: "項目を追加"
clear_all: "すべてクリア"
loading: "読み込み中..."
not_found: "アイテムが見つかりません"
not_found: "見つかりませんでした"
type_to_search: "検索キーワードを入力"
autocomplete_select:
placeholder:
@@ -67,7 +67,7 @@ ja:
button_back_to_list_view: "リスト表示に戻る"
button_cancel: "キャンセル"
button_close: "閉じる"
button_change_project: "のプロジェクトに移"
button_change_project: "のプロジェクトに移"
button_check_all: "全てを選択"
button_configure-form: "フォームを設定"
button_confirm: "確認"
@@ -75,7 +75,7 @@ ja:
button_copy: "コピー"
button_copy_to_clipboard: "クリップボードにコピー"
button_copy_link_to_clipboard: "クリップボードにリンクをコピー"
button_copy_to_other_project: "別のプロジェクトで複製"
button_copy_to_other_project: "別のプロジェクトで複製する"
button_custom-fields: "カスタムフィールド"
button_delete: "削除"
button_delete_watcher: "ウォッチャーを削除"
@@ -97,7 +97,7 @@ ja:
button_open_fullscreen: "全画面表示を開く"
button_show_cards: "カードビュー表示"
button_show_list: "リストビュー表示"
button_show_table: "テーブルビューを表示"
button_show_table: "テーブル表示"
button_show_gantt: "ガントビューを表示"
button_show_fullscreen: "全画面表示"
button_more_actions: "その他の操作"
@@ -107,7 +107,7 @@ ja:
button_uncheck_all: "全てを選択解除"
button_update: "更新"
button_export-atom: "Atomをダウンロード"
button_generate_pdf: "PDFを生成"
button_generate_pdf: "PDF成"
button_create: "作成"
card:
add_new: "新規カード追加"
@@ -141,8 +141,8 @@ ja:
description_select_work_package: "ワークパッケージを選択 #%{id}"
description_subwork_package: "ワークパッケージの子 #%{id}"
editor:
revisions: "ローカルの変更を表示"
no_revisions: "ローカルの変更は見つかりませんでした"
revisions: "ローカルの修正を表示"
no_revisions: "ローカルでの修正は見つからず"
preview: "プレビューモードの切り替え"
source_code: "Markdown ソースモードの切り替え"
error_saving_failed: "次のエラーで文書を保存するのに失敗しました: %{error}"
@@ -155,7 +155,7 @@ ja:
attribute_reference:
macro_help_tooltip: "このテキストセグメントはマクロによって動的にレンダリングされています。"
not_found: "要求されたリソースが見つかりませんでした"
nested_macro: "このマクロは %{model} %{id} を再帰的に参照しています。"
nested_macro: "このマクロは %{model} %{id}を再帰的に参照してい。"
invalid_attribute: "選択した属性 '%{name}' は存在しません。"
child_pages:
button: "子ページへのリンク"
@@ -209,10 +209,10 @@ ja:
calendar:
empty_state_header: "休業日"
empty_state_description: '休業日が定義されていません。「休業日を追加」ボタンをクリックして日付を追加してください。'
new_date: "(新)"
new_date: "(新)"
add_non_working_day: "休業日を追加"
already_added_error: "この日付の非業日はすでに存在します。それぞれの日付に1つの非作業日が作成されます。"
change_button: "保存してスケジュールを変更"
already_added_error: "この日付の非業日はすでに存在します。一意の日付に対して作成できる非営業日は1つだけです。"
change_button: "保存してスケジュール"
change_title: "営業日を変更する"
removed_title: "以下の日を非稼働日リストから削除します:"
change_description: "営業日とみなす曜日を変更すると、このサイト内のすべてのプロジェクトのすべてのワークパッケージの開始日と終了日に影響を与える可能性があります。"
@@ -294,14 +294,14 @@ ja:
ical_sharing_modal:
title: "カレンダーを購読する"
inital_setup_error_message: "データ取得中にエラーが発生しました。"
description: "URLiCalendarを使って外部クライアントでこのカレンダーを購読し、そこから最新のワークパッケージ情報をることができます。"
warning: "このURLを他のユーザーと共有しないでください。このリンクがあれば、誰でもアカウントやパスワードなしでワークパッケージの詳細をることができます。"
token_name_label: "どこで使うのですか?"
description: "URL(iCalendar)を使用して、外部クライアントでこのカレンダーを購読し、そこから最新の作業パッケージ情報を表示することができます。"
warning: "このURLを他のユーザーと共有しないでください。このリンクを持つ誰でもアカウントやパスワードなしでワークパッケージの詳細を表示することができます。"
token_name_label: "どこで使うのですか?"
token_name_placeholder: '名前を入力してください。例:"電話"'
token_name_description_text: 'If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your <a href="%{myAccessTokensUrl}" target="_blank">access tokens</a> list.'
copy_url_label: "URLをコピー"
ical_generation_error_text: "カレンダー URL生成にエラーが発生しました。"
success_message: 'URL "%{name}" は正常にクリップボードにコピーされました。サブスクリプションを完了するためにカレンダークライアントに貼り付けてください。'
ical_generation_error_text: "カレンダーURL生成にエラーが発生しました。"
success_message: 'URL "%{name}" クリップボードにコピーされました。カレンダークライアントに貼り付けて購読を完了してください。'
label_activate: "有効にする"
label_assignee: "担当者"
label_assignee_alt_text: "This work package is assigned to %{name}"
@@ -314,7 +314,7 @@ ja:
label_add_row_before: "前に行を追加"
label_add_selected_columns: "選択した列を追加"
label_added_by: "追加した人"
label_added_time_by: '<a href="%{authorLink}">%{author}</a> %{age} に追加しました'
label_added_time_by: '追加 <a href="%{authorLink}">%{author}</a> %{age}'
label_ago: "○日前"
label_all: "全て"
label_all_projects: "すべてのプロジェクト"
@@ -426,7 +426,7 @@ ja:
label_report: "レポート"
label_repository_plural: "リポジトリ"
label_save_as: "名前をつけて保存"
label_search_columns: "列を検索"
label_search_columns: "列を検索する"
label_select_watcher: "ウォッチャーを選択..."
label_selected_filter_list: "選択されたフィルタ"
label_show_attributes: "すべての属性を表示"
@@ -464,8 +464,8 @@ ja:
label_watch_work_package: "ワークパッケージをウォッチ"
label_watcher_added_successfully: "ウォッチャーが正常に追加されました !"
label_watcher_deleted_successfully: "ウォッチャーが正常に削除されました !"
label_work_package_details_you_are_here: "あなたは %{tab} %{type} %{subject} のタブにいます。"
label_work_package_context_menu: "ワークパッケージのコンテキスト メニュー"
label_work_package_details_you_are_here: "あなたは %{type} %{subject}の %{tab} タブを表示しています。"
label_work_package_context_menu: "作業パッケージのコンテキストメニュー"
label_unwatch: "ウォッチしない"
label_unwatch_work_package: "ワークパッケージのウォッチを削除"
label_uploaded_by: "アップロードした人"
@@ -496,7 +496,7 @@ ja:
label_version_plural: "バージョン"
label_view_has_changed: "このビューには未保存の変更があります。 クリックすると保存します。"
help_texts:
show_modal: "ヘルプテキストを表示"
show_modal: "ヘルプテキストを表示する"
onboarding:
buttons:
skip: "スキップ"
@@ -504,7 +504,7 @@ ja:
got_it: "了承"
steps:
help_menu: "ヘルプ(?)メニューは、<b>その他のヘルプリソースを</b>提供します。ここでは、ユーザーガイド、役立つハウツービデオなどを見つけることができます。 <br> OpenProjectでの作業をお楽しみください!"
members: "新しい <b>メンバー</b>プロジェクトに招待します。"
members: "新しい<b>メンバー</b>プロジェクトに招待す。"
quick_add_button: "ヘッダーナビゲーションにあるプラス(+)アイコンをクリックして、<b>新規プロジェクトを作成</b>したり、<b>同僚を招待</b>したりできます。"
sidebar_arrow: "プロジェクトの<b>メインメニューに</b>戻るには、左上の矢印を使います。"
welcome: "3分間のイントロダクションツアーで、最も<b>重要な</b>機能を学びましょう。 <br> 最後までステップを完了することをお勧めします。ツアーはいつでも再開できます。"
@@ -611,36 +611,36 @@ ja:
work_package_commented: "すべての新着コメント"
work_package_created: "新しいワークパッケージ"
work_package_processed: "すべてのステータス変更"
work_package_prioritized: "すべての優先の変更"
work_package_scheduled: "すべての日付変更"
work_package_prioritized: "すべての優先順位の変更"
work_package_scheduled: "すべての日付変更"
global:
immediately:
title: "参加"
description: "自分が関与しているワークパッケージのすべてのアクティビティに関する通知(アサイニー、アカウンタブル、ウォッチャー)。"
description: "自分が関与しているワークパッケージのすべてのアクティビティに関する通知(担当、責任、ウォッチャー)。"
delayed:
title: "不参加"
description: "すべてのプロジェクトでのアクティビティの追加通知。"
description: "プロジェクトにおける活動の追加通知。"
date_alerts:
title: "日付アラート"
description: "あなたが関与している(アサイニー、アカウンタブル、ウォッチャー)オープンワークパッケージの重要な日付が近づくと自動通知。"
description: "あなたが関与している(担当、責任、ウォッチャー)オープンワークパッケージの重要な日付が近づくと自動通知。"
overdue: 期限を過ぎた場合
project_specific:
title: "プロジェクト固有の通知設定"
description: "これらのプロジェクト固有の設定は、上記のデフォルト設定を上書きす。"
description: "これらのプロジェクト固有の設定は、上記のデフォルト設定を上書きします。"
add: "プロジェクトの設定を追加する"
already_selected: "このプロジェクトは既に選択されています"
already_selected: "このプロジェクトはすでに選ばれてい"
remove: "プロジェクトの設定を削除する"
password_confirmation:
field_description: "この変更を確認するには、アカウントのパスワードを入力する必要があります。"
title: "続行するにはパスワードを確認してください"
pagination:
no_other_page: "このページだけです。"
pages_skipped: "ページスキップされました。"
pages_skipped: "ページスキップ。"
page_navigation: "ページネーション・ナビゲーション"
per_page_navigation: 'ページ毎のアイテム選択'
pages:
page_number: ページ %{number}
show_per_page: ページあたり %{number} を表示
show_per_page: ページごとに %{number}
placeholders:
default: "-"
subject: "ここにタイトルを入力します"
@@ -650,7 +650,7 @@ ja:
project:
autocompleter:
label: "プロジェクト名の入力補完"
click_to_switch_to_project: "プロジェクト: %{projectname}"
click_to_switch_to_project: "プロジェクト %{projectname}"
context: "プロジェクトのコンテキスト"
not_available: "プロジェクトなし"
required_outside_context: >
@@ -658,30 +658,30 @@ ja:
reminders:
settings:
daily:
add_time: "時間を追加"
add_time: "時間を追加する"
enable: "毎日のEメールリマインダーを有効にする"
explanation: "このリマインダーは、未読の通知に対してのみ、指定した時間帯にのみ届きます。 %{no_time_zone}"
no_time_zone: "アカウントにタイムゾーンを設定するまでは、時間はUTCで解釈されます。"
time_label: "時間 %{counter}"
title: "未読通知を毎日メールで通知する"
title: "未読通知メールのリマインダーを毎日送信する"
workdays:
title: "これらの日にリマインダーメールを受け取る"
immediate:
title: "電子メールのリマインダーを送信"
mentioned: "@mentionするとすぐに"
personal_reminder: "個人的なリマインダーを受け取ったら直ちに"
personal_reminder: "個人的なリマインダーを受け取ったとき"
alerts:
title: "その他の項目(ワークパッケージではないもの)に対する電子メールアラート"
explanation: >
本日の通知はワークパッケージに限定されています。これらのイベントが通知に含まれるようになるまで、Eメールアラートを受信し続けることを選択できます:
news_added: "ニュースが追加されました。"
news_commented: "ニュースへのコメント"
document_added: "追加された書類"
document_added: "ドキュメントの追加"
forum_messages: "新しいフォーラムメッセージ"
wiki_page_added: "Wikiページが追加されました。"
wiki_page_updated: "Wikiページが更新されました。"
membership_added: "メンバーシップ追加されました"
membership_updated: "メンバーシップ更新"
membership_added: "メンバーシップ追加"
membership_updated: "メンバーシップ更新"
title: "電子メールによるリマインダー"
pause:
label: "毎日のEメールリマインダーを一時停止する"
@@ -1170,7 +1170,7 @@ ja:
toggle_title: "ベースライン"
clear: "クリア"
apply: "適用"
header_description: "過去のいずれかの時点からこのリストに加えられた変更を強調する。"
header_description: "過去の選択した時点からこのリストに加えられた変更をハイライト"
show_changes_since: "以降の変更を表示する"
help_description: "ベースラインの基準タイムゾーン。"
time_description: "現地時間: %{datetime}"
+1 -1
View File
@@ -104,7 +104,7 @@
button_save: "Lagre"
button_settings: "Innstillinger"
button_uncheck_all: "Avmerk alle"
button_update: "Oppdater"
button_update: "Oppdatèr"
button_export-atom: "Last ned Atom"
button_generate_pdf: "Generate PDF"
button_create: "Opprett"
+1 -1
View File
@@ -104,7 +104,7 @@ ro:
button_save: "Salvează"
button_settings: "Setări"
button_uncheck_all: "Deselectează tot"
button_update: "Actualizează"
button_update: "Actualizare"
button_export-atom: "Descarcă Atom"
button_generate_pdf: "Generează PDF"
button_create: "Creează"
+1 -1
View File
@@ -104,7 +104,7 @@ ru:
button_save: "Сохранить"
button_settings: "Настройки"
button_uncheck_all: "Снять все отметки"
button_update: "Обновить"
button_update: "Обновление"
button_export-atom: "Скачать Atom"
button_generate_pdf: "Создать PDF"
button_create: "Создать"
+2 -2
View File
@@ -3152,7 +3152,7 @@ ro:
label_duplicated_by: "dublat de"
label_duplicate: "duplicat"
label_duplicates: "dublează"
label_edit: "Editează"
label_edit: "Editare"
label_edit_attribute: "Edit attribute"
label_edit_x: "Editare: %{x}"
label_enable_multi_select: "Comutare selecție multiplă"
@@ -3210,7 +3210,7 @@ ro:
label_global_roles: "Roluri globale"
label_git_path: "Calea catre directorul .git"
label_greater_or_equal: ">="
label_group_by: "Grupează după"
label_group_by: "Grupare după"
label_group_new: "Grupare nouă"
label_group: "Grup"
label_group_named: "Grup %{name}"
+2 -2
View File
@@ -2325,8 +2325,8 @@ sl:
- "avgust"
- "september"
- "oktober"
- "november"
- "december"
- "November"
- "December"
order:
- :leto
- :mesec
+1 -1
View File
@@ -3278,7 +3278,7 @@ uk:
label_index_by_title: "Індекс за назвою"
label_information: "Інформація"
label_information_plural: "Інформація"
label_installation_guides: "Інструкції із встановлення"
label_installation_guides: "Інструкції зі встановлення"
label_integer: "Ціле число"
label_interface: "Інтерфейс"
label_internal: "Власне"
+2 -2
View File
@@ -97,7 +97,7 @@ zh-CN:
demo-project:
name: 演示项目
status_explanation: 所有任务都按计划进行。相关人员均知晓各自任务。系统已完全建立。
description: 这是对此演示 Scrum 项目目标的简短摘要。
description: 这是对此演示项目目标的简短摘要。
news:
item_0:
title: 欢迎来到您的演示项目
@@ -216,7 +216,7 @@ zh-CN:
scrum-project:
name: Scrum 项目
status_explanation: 所有任务都按计划进行。相关人员均知晓各自任务。系统已完全建立。
description: 这是对此演示 Scrum 项目目标的简短摘要。
description: 这是对此演示Scrum项目目标的简短摘要。
news:
item_0:
title: 欢迎来到您的 Scrum 演示项目
+6 -6
View File
@@ -87,7 +87,7 @@ zh-CN:
token_placeholder: "在此处粘贴您的企业版支持令牌"
add_token: "上传企业版支持令牌"
replace_token: "替换您当前的支持令牌"
order: "订购本地部署的 Enterprise edition"
order: "订购本地部署的 Enterprise edition"
paste: "粘贴您企业版的支持令牌"
required_for_feature: "此功能仅限具激活的企业版支持令牌的订阅者使用。"
enterprise_link: "如需了解详细信息,请单击此处。"
@@ -1163,7 +1163,7 @@ zh-CN:
page: "页"
row_count: "行数"
column_count: "列数"
widgets: "件"
widgets: "小部件"
journal:
notes: "备注"
cause_type: "Cause 类型"
@@ -3395,7 +3395,7 @@ zh-CN:
label_revision_id: "修订版本 %{value}"
label_revision_plural: "修订"
label_roadmap: "路线图"
label_roadmap_edit: "编辑路线图%{name}"
label_roadmap_edit: "编辑路线图 %{name}"
label_roadmap_due_in: "%{value} 到期"
label_roadmap_no_work_packages: "该版本没有工作包。"
label_roadmap_overdue: "%{value} 超时"
@@ -4118,7 +4118,7 @@ zh-CN:
managed: "在 OpenProject 中创建新的存储库"
storage:
not_available: "磁盘存储开销不可用于此存储库。"
update_timeout: "在 N 分钟内保留存储库最后所需磁盘空间信息。由于计算存储库所需的磁盘空间可能增加系统开销,增加该值可以减少性能影响。"
update_timeout: "在 N 分钟内保留存储库最后所需磁盘空间信息。由于计算存储库所需的磁盘空间可能增加系统开销,增加该值可以减少性能影响。"
oauth_application_details: "关闭此窗口后,将无法再次访问客户端密钥值。请将这些值复制到 Nextcloud OpenProject 集成设置中:"
oauth_application_details_link_text: "转到设置页面"
setup_documentation_details: "如果您在配置新文件存储方面需要帮助,请查看文档:"
@@ -4316,7 +4316,7 @@ zh-CN:
setting_session_ttl_hint: "当设置的值低于5时,其作用类似于禁用。"
setting_session_ttl_enabled: "会话过期"
setting_start_of_week: "一周起始日"
setting_sys_api_enabled: "启用存储库管理网页服务"
setting_sys_api_enabled: "启用版本库管理 web 服务"
setting_sys_api_description: "存储库管理网页服务提供了集成的,用户授权的存储库访问。"
setting_time_format: "时间"
setting_total_percent_complete_mode: "计算 完成% 层次结构总数"
@@ -4738,7 +4738,7 @@ zh-CN:
warning_user_limit_reached_admin: >
添加额外的用户将超出当前限制。请<a href="%{upgrade_url}">升级您的计划</a>,以确保外部用户能够访问此实例。
warning_user_limit_reached_instructions: >
达到用户限制(%{current}/%{max} 活跃用户)。请联系 sales@openproject.com 升级您的企业版计划添加额外用户。
您达到用户限制(%{current}/%{max}活跃用户)。 请联系sales@openproject.com升级您的Enterprise edition计划添加其他用户。
warning_protocol_mismatch_html: >
warning_bar:
+5 -5
View File
@@ -3096,7 +3096,7 @@ zh-TW:
label_filter_add: "新增條件"
label_filter_by: "篩選條件:"
label_filter_any_name_attribute: "名稱屬性"
label_filter_plural: "篩選條件"
label_filter_plural: "篩選"
label_filters_toggle: "顯示/隱藏篩選條件"
label_float: "浮點數"
label_folder: "資料夾"
@@ -3111,8 +3111,8 @@ zh-TW:
label_global_modules: "全域模組"
label_global_roles: "全域角色"
label_git_path: ".git 目錄的路徑"
label_greater_or_equal: "之前"
label_group_by: "分"
label_greater_or_equal: ">="
label_group_by: "分組依據"
label_group_new: "新增群組"
label_group: "群組"
label_group_named: "群組名稱 %{name}"
@@ -3124,7 +3124,7 @@ zh-TW:
label_hierarchy: "階層"
label_hierarchy_leaf: "頁面結構頁"
label_home: "Home"
label_subject_or_id: "名稱或 id"
label_subject_or_id: "主旨或 id"
label_calendar_subscriptions: "訂閱行事曆"
label_identifier: "識別碼"
label_in: "在"
@@ -3173,7 +3173,7 @@ zh-TW:
label_latest_revision_plural: "最新版本"
label_ldap_authentication: "LDAP 認證"
label_learn_more: "了解更多"
label_less_or_equal: "之後"
label_less_or_equal: "<="
label_less_than_ago: "幾天內"
label_link_url: "連結(URL)"
label_list: "清單"
+4 -1
View File
@@ -1566,7 +1566,8 @@ en:
not_a_datetime: "is not a valid date time."
not_a_number: "is not a number."
not_allowed: "is invalid because of missing permissions."
not_json: "is not a valid JSON object."
not_json: "is not parseable as JSON."
not_json_object: "is not a JSON object."
not_an_integer: "is not an integer."
not_an_iso_date: "is not a valid date. Required format: YYYY-MM-DD."
not_same_project: "doesn't belong to the same project."
@@ -2234,6 +2235,7 @@ en:
button_print: "Print"
button_quote: "Quote"
button_remove: Remove
button_remove_permanently: "Remove permanently"
button_remove_reminder: "Remove reminder"
button_rename: "Rename"
button_replace: "Replace"
@@ -4949,6 +4951,7 @@ en:
For more information on what the check provides, what data is needed to provide available updates, and how to disable this check, please visit <a href="%{more_info_url}">the configuration documentation</a>.
text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?"
text_permanent_delete_confirmation_checkbox_label: "I understand that this deletion cannot be reversed"
text_permanent_remove_confirmation_checkbox_label: "I understand that this removal cannot be reversed"
text_plugin_assets_writable: "Plugin assets directory writable"
text_powered_by: "Powered by %{link}"
text_project_identifier_info: "Only lower case letters (a-z), numbers, dashes and underscores are allowed, must start with a lower case letter."
+1202 -294
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -14,8 +14,8 @@
"@angular-eslint/template-parser": "20.7.0",
"@angular/language-service": "20.3.14",
"@eslint/js": "^9.39.1",
"@html-eslint/eslint-plugin": "^0.49.0",
"@html-eslint/parser": "^0.49.0",
"@html-eslint/eslint-plugin": "^0.50.0",
"@html-eslint/parser": "^0.50.0",
"@jsdevtools/coverage-istanbul-loader": "3.0.5",
"@stylistic/eslint-plugin": "^5.5.0",
"@types/codemirror": "5.60.5",
@@ -23,7 +23,7 @@
"@types/dragula": "^3.7.5",
"@types/flot": "^0.0.36",
"@types/hammerjs": "^2.0.36",
"@types/jasmine": "~5.1.12",
"@types/jasmine": "~5.1.13",
"@types/jquery": "^3.5.33",
"@types/jqueryui": "^1.12.24",
"@types/lodash": "^4.17.21",
@@ -38,7 +38,7 @@
"@types/webpack-env": "^1.16.0",
"@typescript-eslint/eslint-plugin": "8.46.3",
"@typescript-eslint/parser": "8.46.2",
"angular-eslint": "^20.5.1",
"angular-eslint": "^21.0.1",
"browserslist": "^4.27.0",
"eslint": "^9.39.1",
"eslint-plugin-import": "^2.32.0",
@@ -59,7 +59,7 @@
"source-map-explorer": "^2.5.2",
"ts-node": "~10.9.2",
"typescript": "^5.8.3",
"typescript-eslint": "^8.46.2",
"typescript-eslint": "^8.48.0",
"webpack-bundle-analyzer": "^4.4.2",
"wscat": "^6.1.0"
},
@@ -134,14 +134,14 @@
"chartjs-plugin-datalabels": "^2.2.0",
"codemirror": "^5.62.0",
"copy-text-to-clipboard": "^3.2.2",
"core-js": "^3.46.0",
"core-js": "^3.47.0",
"crossvent": "^1.5.4",
"dom-autoscroller": "^2.2.8",
"dom-plane": "^1.0.2",
"dragula": "^3.7.3",
"es6-slide-up-down": "^1.0.0",
"flatpickr": "^4.6.13",
"glob": "^12.0.0",
"glob": "^13.0.0",
"hammerjs": "^2.0.8",
"i18n-js": "^4.3.0",
"idiomorph": "^0.7.4",
@@ -51,6 +51,13 @@ export default class extends Controller {
useMeta(this, { suffix: false });
this.focusInput();
this.addNotes();
this.storeInitialValues();
}
storeInitialValues():void {
this.element.querySelectorAll('input[type="text"], input[type="number"]').forEach((input) => {
(input as HTMLInputElement).dataset.initialValue = (input as HTMLInputElement).value;
});
}
focusInput():void {
@@ -41,8 +41,20 @@ export function appendCollapsedState(
}
export function hasUnsavedChanges():boolean {
const textInputs = Array.from(document.querySelectorAll('input[type="text"], input[type="number"]'));
const allTextSaved = textInputs.every((input) => (input as HTMLInputElement).value.trim().length === 0);
let hasTextChanges = false;
return !allTextSaved || window.OpenProject.pageWasEdited;
document.querySelectorAll('input[type="text"], input[type="number"]').forEach((input) => {
const currentValue = (input as HTMLInputElement).value;
const initialValue = (input as HTMLInputElement).dataset.initialValue;
if (initialValue === undefined) {
if (currentValue.trim().length > 0) {
hasTextChanges = true;
}
} else if (currentValue !== initialValue) {
hasTextChanges = true;
}
});
return hasTextChanges || window.OpenProject.pageWasEdited;
}
+1 -1
View File
@@ -32,7 +32,7 @@ require "open3"
module OpenProject
module VERSION # :nodoc:
MAJOR = 17
MINOR = 0
MINOR = 1
PATCH = 0
class << self
+7 -2
View File
@@ -273,8 +273,9 @@ module OpenProject
request_headers)
header = %{#{scheme} realm="#{scope_realm(scope)}"}
header << %{, error="#{error}"} if error
header << %{, error_description="#{error_description}"} if error && error_description
header << %{, scope="#{escape_string scope}"} if scope && scheme == "Bearer"
header << %{, error="#{escape_string error}"} if error
header << %{, error_description="#{escape_string error_description}"} if error && error_description
header
end
@@ -285,6 +286,10 @@ module OpenProject
.select { |_, info| scope.nil? or info.strategies.intersect?(strategies) }
.keys
end
def escape_string(string)
string.to_s.dump[1..-2]
end
end
module AuthHeaders
@@ -37,6 +37,7 @@ module OpenProject
headers(
"WWW-Authenticate" => OpenProject::Authentication::WWWAuthenticate.response_header(
default_auth_scheme: "Bearer",
scope:,
error:,
error_description:
)
@@ -52,7 +52,24 @@ module Redmine
validate: false,
autosave: true
validation_options = options[:validate_on] ? { on: options[:validate_on] } : {}
validation_options = {}
if options[:validate_on]
validation_options[:on] = options[:validate_on]
end
if options[:validate_except_on]
validation_options[:except_on] = options[:validate_except_on]
end
if options[:validate_if]
validation_options[:if] = options[:validate_if]
end
if options[:validate_unless]
validation_options[:unless] = options[:validate_unless]
end
validate :validate_custom_values, **validation_options
send :include, Redmine::Acts::Customizable::InstanceMethods
@@ -28,7 +28,7 @@ ro:
work_package:
position: "Poziție"
story_points: "Puncte"
backlogs_work_package_type: "Tipul de restante"
backlogs_work_package_type: "Tip restanță"
errors:
models:
work_package:
@@ -21,7 +21,7 @@
#++
zh-TW:
plugin_openproject_backlogs:
name: "OpenProject辦事項"
name: "OpenProject辦事項"
description: "此模組新增了讓敏捷團隊能夠在 Scrum 專案中使用 OpenProject 的功能。"
activerecord:
attributes:
+1 -1
View File
@@ -59,7 +59,7 @@ fr:
perform_description: "Voulez-vous importer ou mettre à jour les problèmes repris ci-dessus ?"
replace_with_system_user: 'Les remplacer par l''utilisateur "Système"'
import_as_system_user: 'Les importer comme utilisateur "Système".'
what_to_do: "Que voulez-vous faire?"
what_to_do: "Que voulez-vous faire?"
work_package_has_newer_changes: "Obsolète ! Ce sujet n'a pas été mis à jour, car les derniers changements sur le serveur étaient plus récents que la \"ModifiedDate\" du sujet importé. Toutefois, les commentaires sur le sujet ont été importés."
bcf_file_not_found: "Impossible de localiser le fichier BCF. Veuillez recommencer le processus de téléversement."
export:
@@ -27,7 +27,7 @@ cs:
budget:
author: "Autor"
available: "Dostupné"
budget: "Plánované"
budget: "Rozpočet"
budget_ratio: "Stráveno (poměr)"
description: "Popis"
spent: "Strávený čas"
+1 -1
View File
@@ -44,7 +44,7 @@ class TimeEntry < ApplicationRecord
MAX_TIME = (60 * 24) - 1 # => 23:59
SECONDS_PER_HOUR = 3600.0
acts_as_customizable
acts_as_customizable validate_unless: ->(te) { te.new_record? && te.ongoing? }
acts_as_journalized
+1 -1
View File
@@ -205,7 +205,7 @@ ja:
setting_enforce_tracking_start_and_end_times: "開始/終了時間を必須とする"
setting_enforce_without_allow: "開始時間と終了時間を要求することは許可されていないとできません"
setting_allow_tracking_start_and_end_times_caption: "時間を記録する際に、開始時間と終了時間を入力できるようにする。"
setting_enforce_tracking_start_and_end_times_caption: "時間を記録する際、開始時間と終了時間の入力必須にします。"
setting_enforce_tracking_start_and_end_times_caption: "時間を記録する際、開始時間と終了時間の入力必須となる。"
text_assign_time_and_cost_entries_to_project: "報告された時間とコストをプロジェクトに割り当てる"
text_destroy_cost_entries_question: "削除しようとしているワークパッケージが%{cost_entries} 件報告されました。どうしますか?"
text_destroy_time_and_cost_entries: "報告された時間とコストを削除する"
@@ -663,5 +663,64 @@ RSpec.describe TimeEntry do
comments: "lorem")
end
let!(:custom_field) { create(:time_entry_custom_field, :string, is_required: false) }
context "with a required custom field" do
let!(:custom_field) { create(:time_entry_custom_field, :string, is_required: true) }
before do
new_model_instance.activate_custom_field_validations!
end
context "for a new not-ongoing entry" do
let!(:new_model_instance) do
build(:time_entry, entity: work_package, spent_on: date, hours:, user:, ongoing: false)
end
it "validates presence of the custom field" do
new_model_instance.custom_field_values = { custom_field.id => nil }
expect(new_model_instance).not_to be_valid
new_model_instance.custom_field_values = { custom_field.id => "Some value" }
expect(new_model_instance).to be_valid
end
end
context "for a new ongoing entry" do
let!(:new_model_instance) do
build(:time_entry, entity: work_package, spent_on: date, hours:, user:, ongoing: true)
end
it "does not validate presence of the custom field" do
new_model_instance.custom_field_values = { custom_field.id => nil }
expect(new_model_instance).to be_valid
end
end
context "for a persisted not-ongoing entry" do
let!(:new_model_instance) do
create(:time_entry, entity: work_package, spent_on: date, hours:, user:, ongoing: false)
end
it "validates presence of the custom field" do
new_model_instance.custom_field_values = { custom_field.id => nil }
expect(new_model_instance).not_to be_valid
new_model_instance.custom_field_values = { custom_field.id => "Some value" }
expect(new_model_instance).to be_valid
end
end
context "for a persisted ongoing entry" do
let!(:new_model_instance) do
create(:time_entry, entity: work_package, spent_on: date, hours:, user:, ongoing: true)
end
it "validates presence of the custom field" do
new_model_instance.custom_field_values = { custom_field.id => nil }
expect(new_model_instance).not_to be_valid
end
end
end
end
end
@@ -60,17 +60,23 @@ rw:
merge_request_reopened_comment: >
**MR Reopened:** Merge request %{mr_number} [%{mr_title}](%{mr_url}) for [%{repository}](%{repository_url}) has been reopened by [%{gitlab_user}](%{gitlab_user_url}).
note_commit_referenced_comment: >
**Referenced in Commit:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in a Commit Note [%{commit_id}](%{commit_url}) on [%{repository}](%{repository_url}): %{commit_note}
**Referenced in Commit:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in a Commit Note [%{commit_id}](%{commit_url}) on [%{repository}](%{repository_url}):
%{commit_note}
note_mr_referenced_comment: >
**Referenced in MR:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in Merge Request %{mr_number} [%{mr_title}](%{mr_url}) on [%{repository}](%{repository_url}): %{mr_note}
**Referenced in MR:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in Merge Request %{mr_number} [%{mr_title}](%{mr_url}) on [%{repository}](%{repository_url}):
%{mr_note}
note_mr_commented_comment: >
**Commented in MR:** [%{gitlab_user}](%{gitlab_user_url}) commented this WP in Merge Request %{mr_number} [%{mr_title}](%{mr_url}) on [%{repository}](%{repository_url}): %{mr_note}
**Commented in MR:** [%{gitlab_user}](%{gitlab_user_url}) commented this WP in Merge Request %{mr_number} [%{mr_title}](%{mr_url}) on [%{repository}](%{repository_url}):
%{mr_note}
note_issue_referenced_comment: >
**Referenced in Issue:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in Issue %{issue_number} [%{issue_title}](%{issue_url}) on [%{repository}](%{repository_url}): %{issue_note}
**Referenced in Issue:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in Issue %{issue_number} [%{issue_title}](%{issue_url}) on [%{repository}](%{repository_url}):
%{issue_note}
note_issue_commented_comment: >
**Commented in Issue:** [%{gitlab_user}](%{gitlab_user_url}) commented this WP in Issue %{issue_number} [%{issue_title}](%{issue_url}) on [%{repository}](%{repository_url}): %{issue_note}
**Commented in Issue:** [%{gitlab_user}](%{gitlab_user_url}) commented this WP in Issue %{issue_number} [%{issue_title}](%{issue_url}) on [%{repository}](%{repository_url}):
%{issue_note}
note_snippet_referenced_comment: >
**Referenced in Snippet:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in Snippet %{snippet_number} [%{snippet_title}](%{snippet_url}) on [%{repository}](%{repository_url}): %{snippet_note}
**Referenced in Snippet:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in Snippet %{snippet_number} [%{snippet_title}](%{snippet_url}) on [%{repository}](%{repository_url}):
%{snippet_note}
issue_opened_referenced_comment: >
**Issue Opened:** Issue %{issue_number} [%{issue_title}](%{issue_url}) for [%{repository}](%{repository_url}) has been opened by [%{gitlab_user}](%{gitlab_user_url}).
issue_closed_referenced_comment: >
@@ -78,8 +84,11 @@ rw:
issue_reopened_referenced_comment: >
**Issue Reopened:** Issue %{issue_number} [%{issue_title}](%{issue_url}) for [%{repository}](%{repository_url}) has been reopened by [%{gitlab_user}](%{gitlab_user_url}).
push_single_commit_comment: >
**Pushed in MR:** [%{gitlab_user}](%{gitlab_user_url}) pushed [%{commit_number}](%{commit_url}) to [%{repository}](%{repository_url}) at %{commit_timestamp}: %{commit_note}
**Pushed in MR:** [%{gitlab_user}](%{gitlab_user_url}) pushed [%{commit_number}](%{commit_url}) to [%{repository}](%{repository_url}) at %{commit_timestamp}:
%{commit_note}
push_single_commit_comment_with_ref: >
**Pushed in %{reference}:** [%{gitlab_user}](%{gitlab_user_url}) pushed [%{commit_number}](%{commit_url}) to [%{repository}](%{repository_url}) at %{commit_timestamp}: %{commit_note}
**Pushed in %{reference}:** [%{gitlab_user}](%{gitlab_user_url}) pushed [%{commit_number}](%{commit_url}) to [%{repository}](%{repository_url}) at %{commit_timestamp}:
%{commit_note}
push_multiple_commits_comment: >
**Pushed in MR:** [%{gitlab_user}](%{gitlab_user_url}) pushed multiple commits [%{commit_number}](%{commit_url}) to [%{repository}](%{repository_url}) at %{commit_timestamp}: %{commit_note}
**Pushed in MR:** [%{gitlab_user}](%{gitlab_user_url}) pushed multiple commits [%{commit_number}](%{commit_url}) to [%{repository}](%{repository_url}) at %{commit_timestamp}:
%{commit_note}
@@ -60,17 +60,23 @@ uz:
merge_request_reopened_comment: >
**MR Reopened:** Merge request %{mr_number} [%{mr_title}](%{mr_url}) for [%{repository}](%{repository_url}) has been reopened by [%{gitlab_user}](%{gitlab_user_url}).
note_commit_referenced_comment: >
**Referenced in Commit:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in a Commit Note [%{commit_id}](%{commit_url}) on [%{repository}](%{repository_url}): %{commit_note}
**Referenced in Commit:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in a Commit Note [%{commit_id}](%{commit_url}) on [%{repository}](%{repository_url}):
%{commit_note}
note_mr_referenced_comment: >
**Referenced in MR:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in Merge Request %{mr_number} [%{mr_title}](%{mr_url}) on [%{repository}](%{repository_url}): %{mr_note}
**Referenced in MR:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in Merge Request %{mr_number} [%{mr_title}](%{mr_url}) on [%{repository}](%{repository_url}):
%{mr_note}
note_mr_commented_comment: >
**Commented in MR:** [%{gitlab_user}](%{gitlab_user_url}) commented this WP in Merge Request %{mr_number} [%{mr_title}](%{mr_url}) on [%{repository}](%{repository_url}): %{mr_note}
**Commented in MR:** [%{gitlab_user}](%{gitlab_user_url}) commented this WP in Merge Request %{mr_number} [%{mr_title}](%{mr_url}) on [%{repository}](%{repository_url}):
%{mr_note}
note_issue_referenced_comment: >
**Referenced in Issue:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in Issue %{issue_number} [%{issue_title}](%{issue_url}) on [%{repository}](%{repository_url}): %{issue_note}
**Referenced in Issue:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in Issue %{issue_number} [%{issue_title}](%{issue_url}) on [%{repository}](%{repository_url}):
%{issue_note}
note_issue_commented_comment: >
**Commented in Issue:** [%{gitlab_user}](%{gitlab_user_url}) commented this WP in Issue %{issue_number} [%{issue_title}](%{issue_url}) on [%{repository}](%{repository_url}): %{issue_note}
**Commented in Issue:** [%{gitlab_user}](%{gitlab_user_url}) commented this WP in Issue %{issue_number} [%{issue_title}](%{issue_url}) on [%{repository}](%{repository_url}):
%{issue_note}
note_snippet_referenced_comment: >
**Referenced in Snippet:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in Snippet %{snippet_number} [%{snippet_title}](%{snippet_url}) on [%{repository}](%{repository_url}): %{snippet_note}
**Referenced in Snippet:** [%{gitlab_user}](%{gitlab_user_url}) referenced this WP in Snippet %{snippet_number} [%{snippet_title}](%{snippet_url}) on [%{repository}](%{repository_url}):
%{snippet_note}
issue_opened_referenced_comment: >
**Issue Opened:** Issue %{issue_number} [%{issue_title}](%{issue_url}) for [%{repository}](%{repository_url}) has been opened by [%{gitlab_user}](%{gitlab_user_url}).
issue_closed_referenced_comment: >
@@ -78,8 +84,11 @@ uz:
issue_reopened_referenced_comment: >
**Issue Reopened:** Issue %{issue_number} [%{issue_title}](%{issue_url}) for [%{repository}](%{repository_url}) has been reopened by [%{gitlab_user}](%{gitlab_user_url}).
push_single_commit_comment: >
**Pushed in MR:** [%{gitlab_user}](%{gitlab_user_url}) pushed [%{commit_number}](%{commit_url}) to [%{repository}](%{repository_url}) at %{commit_timestamp}: %{commit_note}
**Pushed in MR:** [%{gitlab_user}](%{gitlab_user_url}) pushed [%{commit_number}](%{commit_url}) to [%{repository}](%{repository_url}) at %{commit_timestamp}:
%{commit_note}
push_single_commit_comment_with_ref: >
**Pushed in %{reference}:** [%{gitlab_user}](%{gitlab_user_url}) pushed [%{commit_number}](%{commit_url}) to [%{repository}](%{repository_url}) at %{commit_timestamp}: %{commit_note}
**Pushed in %{reference}:** [%{gitlab_user}](%{gitlab_user_url}) pushed [%{commit_number}](%{commit_url}) to [%{repository}](%{repository_url}) at %{commit_timestamp}:
%{commit_note}
push_multiple_commits_comment: >
**Pushed in MR:** [%{gitlab_user}](%{gitlab_user_url}) pushed multiple commits [%{commit_number}](%{commit_url}) to [%{repository}](%{repository_url}) at %{commit_timestamp}: %{commit_note}
**Pushed in MR:** [%{gitlab_user}](%{gitlab_user_url}) pushed multiple commits [%{commit_number}](%{commit_url}) to [%{repository}](%{repository_url}) at %{commit_timestamp}:
%{commit_note}
@@ -6,7 +6,7 @@ zh-CN:
description: '与 OpenProject 组同步 LDAP 组以管理用户,更改他们的权限以便不同组的用户管理。'
plugin_openproject_ldap_groups:
name: "OpenProject LDAP 组"
description: "LDAP组成员同步。"
description: "LDAP 组成员同步。"
activerecord:
attributes:
ldap_groups/synchronized_group:
@@ -15,7 +15,7 @@ zh-TW:
ldap_auth_source: 'LDAP 連線'
sync_users: '同步使用者'
ldap_groups/synchronized_filter:
filter_string: 'LDAP篩選條件'
filter_string: '簡約登入目錄制約(LDAP)篩選'
auth_source: '驗證來源'
ldap_auth_source: 'LDAP 連線'
group_name_attribute: "群組名字屬性"
@@ -1,35 +1,37 @@
<%=
flex_layout(align_items: :center, justify_content: :space_between) do |row|
row.with_column(flex: 1, classes: "ellipsis") do
render(
Users::AvatarComponent.new(
user: @participant.user,
classes: "op-principal_flex",
hover_card: { active: true, target: :custom }
)
)
end
row.with_column do
flex_layout(align_items: :center, justify_content: :space_between) do |actions|
if @meeting.in_progress?
actions.with_column(ml: 2) do
render(Meetings::Participants::AttendedToggleButtonComponent.new(meeting: @meeting, participant: @participant))
end
end
actions.with_column(ml: 2) do
render(
Primer::Beta::IconButton.new(
scheme: :invisible,
tag: :a,
href: meeting_participant_path(@meeting, @participant),
icon: :x,
aria: { label: I18n.t("meeting.participants.label.remove_participant") },
data: {
turbo_method: :delete,
test_selector: "remove_button_#{@participant.user_id}"
}
)
component_wrapper do
flex_layout(align_items: :center, justify_content: :space_between) do |row|
row.with_column(flex: 1, classes: "ellipsis") do
render(
Users::AvatarComponent.new(
user: @participant.user,
classes: "op-principal_flex",
hover_card: { active: true, target: :custom }
)
)
end
row.with_column do
flex_layout(align_items: :center, justify_content: :space_between) do |actions|
if @meeting.in_progress?
actions.with_column(ml: 2) do
render(Meetings::Participants::AttendedToggleButtonComponent.new(meeting: @meeting, participant: @participant))
end
end
actions.with_column(ml: 2) do
render(
Primer::Beta::IconButton.new(
scheme: :invisible,
tag: :a,
href: meeting_participant_path(@meeting, @participant),
icon: :x,
aria: { label: I18n.t("meeting.participants.label.remove_participant") },
data: {
turbo_method: :delete,
test_selector: "remove_button_#{@participant.user_id}"
}
)
)
end
end
end
end
@@ -41,5 +41,9 @@ module Meetings
@meeting = meeting
@participant = participant
end
def wrapper_uniq_by
@participant.id
end
end
end
@@ -95,6 +95,12 @@ module Meetings
)
end
def update_box_row_component_via_turbo_stream(participant:, meeting: @meeting)
update_via_turbo_stream(
component: Meetings::Participants::BoxRowComponent.new(meeting:, participant:)
)
end
def update_show_items_via_turbo_stream(meeting: @meeting, current_occurrence: nil)
meeting.sections.each do |meeting_section|
update_show_items_of_section_via_turbo_stream(meeting_section:, current_occurrence:)
@@ -65,8 +65,7 @@ class MeetingParticipantsController < ApplicationController
def toggle_attendance
@participant.toggle!(:attended)
update_add_user_form_component_via_turbo_stream
update_list_component_via_turbo_stream
update_box_row_component_via_turbo_stream(participant: @participant)
update_sidebar_participants_component_via_turbo_stream(meeting: @meeting)
respond_with_turbo_streams
@@ -35,7 +35,7 @@ module RecurringMeetings
protected
def validate_params
@old_schedule = model.full_schedule_in_words
@old_schedule_model = model.dup
@old_location = model.template.location
super
end
@@ -159,11 +159,16 @@ module RecurringMeetings
.participants
.invited
.find_each do |participant|
# Generate old schedule in each participant's locale
old_schedule = User.execute_as(participant.user) do
@old_schedule_model.full_schedule_in_words
end
MeetingSeriesMailer.updated(
recurring_meeting,
participant.user,
User.current,
changes: { old_schedule: @old_schedule, old_location: @old_location }
changes: { old_schedule:, old_location: @old_location }
).deliver_now
end
end
@@ -466,7 +466,7 @@ cs:
notice_meeting_updated: "Tato stránka byla aktualizována někým jiným. Pro zobrazení změn znovu načtena."
permission_create_meetings: "Vytvořit schůzku\n"
permission_edit_meetings: "Upravit schůzku"
permission_delete_meetings: "Odstranit schůzky"
permission_delete_meetings: "Smazat schůzku"
permission_view_meetings: "Zobrazit schůzky"
permission_manage_agendas: "Správa zápisů"
permission_manage_agendas_explanation: "Allows creating, editing and removing agenda items"
@@ -227,9 +227,9 @@ ja:
header_occurrence: "キャンセル: ミーティング発生'%{title}'"
header_series: "キャンセル: ミーティングシリーズ '%{title}'"
summary_occurrence: "%{title}'の発生は %{actor}によってキャンセルされました。"
summary_series: "ミーティングシリーズ '%{title}' は %{actor}によキャンセルされました。"
summary: "'%{title}' は %{actor}によってキャンセルされた。"
date_time: "予定日時"
summary_series: "ミーティングシリーズ '%{title}' は %{actor} によってキャンセルされました。"
summary: "'%{title}' は %{actor} によってキャンセルされました。"
date_time: "スケジュールされた日時"
updated:
header: "Meeting '%{title}' has been updated"
summary: "Meeting '%{title}' has been updated by %{actor}"
@@ -256,7 +256,7 @@ ja:
title: "ミーティングのキャンセル"
heading: "このミーティングをキャンセルしますか?"
confirmation_message_html: >
テンプレートにない会議情報は失われます。 続行しますか?
テンプレートにない会議情報は失われます。 続ますか?
confirm_button: "発生をキャンセル"
blankslate:
title: "表示する会議がありません"
@@ -440,7 +440,7 @@ ja:
confirm_button: "この予定をキャンセル"
end_series_dialog:
title: "一連の会議を終了"
notice_successful_notification: "参加者全員にカレンダー更新の電子メールを送信"
notice_successful_notification: "すべての出席者にカレンダー更新をメールしました"
notice_timezone_missing: タイムゾーンが設定されていない場合、%{zone} が使用されます。タイムゾーンを選択するには、ここをクリックしてください。
notice_meeting_updated: "このページは他の誰かによって更新されました。変更を表示するには再読み込みしてください。"
permission_create_meetings: "会議を作成"
@@ -519,7 +519,7 @@ ja:
このバックログは、このワンタイムミーティングに固有のものです.アイテムをドラッグして追加またはミーティングの議題から削除することができます.
label_agenda_backlog_clear_title: "議題のバックログをクリアしますか?"
text_agenda_backlog_clear_description: >
現在アジェンダバックログにあるすべての項目を削除してもよろしいですか?このアクションは元に戻せません。
議題のバックログ内のすべての項目を削除してもよろしいですか?この操作は取り消せません。
label_series_backlog: "シリーズバックログ"
text_series_backlog: >
バックログはこのシリーズのすべての出現と共有されます。 項目をドラッグして、特定のミーティングから項目を追加または削除できます。
@@ -551,7 +551,7 @@ ja:
text_meeting_closed_description: "この会議は終了しています。これ以上、議題項目の追加/削除はできません。"
text_meeting_in_progress_description: "議題を変更したり、各項目のアウトカムを記録したり、参加者の出席を追跡することができます。 ミーティングが完了すると、ミーティングをクローズとしてマークしてロックできます。"
text_meeting_open_dropdown_description: "既存の結果は残りますが、ユーザーは新しい結果を追加することはできません。"
text_meeting_in_progress_dropdown_description: "会議中に必要な情報や決定事項などの成果を文書化する。"
text_meeting_in_progress_dropdown_description: "会議中に取られた情報のニーズや意思決定などの成果を記録します。"
text_meeting_closed_dropdown_description: "この会議は終了しました。これ以上、議題や結果を変更することはできません。"
text_meeting_draft_banner: "You are currently in draft mode. This meeting will not send out any calendar updates or invites, even if you change meeting details or add/remove participants."
text_exit_draft_mode_dialog_title: "Open this meeting and send invites?"
@@ -87,4 +87,42 @@ RSpec.describe "Meetings edit agenda", :js do
expect(show_page).to have_selector :rich_text, "Notes", text: "More notes..."
end
end
it "correctly tracks unsaved changes in agenda item forms (Bug #68654)" do
show_page.add_agenda_item do
fill_in "Title", with: "First Item"
end
show_page.add_agenda_item do
fill_in "Title", with: "Second Item"
end
show_page.expect_agenda_item title: "First Item"
show_page.expect_agenda_item title: "Second Item"
show_page.assert_agenda_order! "First Item", "Second Item"
first_item = MeetingAgendaItem.find_by(title: "First Item")
second_item = MeetingAgendaItem.find_by(title: "Second Item")
show_page.select_action(second_item, "Edit")
# No confirmation when title isn't changed
expect do
accept_confirm do
show_page.select_action(first_item, I18n.t(:label_sort_lowest))
end
end.to raise_error(Capybara::ModalNotFound)
show_page.assert_agenda_order! "Second Item", "First Item"
second_item.reload
show_page.edit_agenda_item(second_item, save: false) do
fill_in "Title", with: "Modified Second Item"
end
# Confirmation when title is changed
dismiss_confirm do
show_page.select_action(first_item, I18n.t(:label_sort_highest))
end
end
end
@@ -192,6 +192,54 @@ RSpec.describe RecurringMeetings::UpdateService, "integration", type: :model do
.to eq "[#{project.name}] Meeting series '#{series.title}' has been updated"
end
end
context "when updating only the location with recipients with different locales" do
let(:german_author) do
create(:user,
language: "de",
member_with_permissions: { project => %i(view_meetings edit_meetings) })
end
let(:english_recipient) do
create(:user,
language: "en",
member_with_permissions: { project => %i(view_meetings) })
end
let(:german_recipient) do
create(:user,
language: "de",
member_with_permissions: { project => %i(view_meetings) })
end
let(:instance) { described_class.new(model: series, user: german_author) }
let(:params) { { location: "New location" } }
before do
series.update!(author: german_author)
series.template.participants.delete_all
series.template.participants << MeetingParticipant.new(user: english_recipient, invited: true)
series.template.participants << MeetingParticipant.new(user: german_recipient, invited: true)
series.template.update!(location: "Old location")
end
it "does not send a schedule update when not necessary (Bug #67287)" do
expect(service_result).to be_success
perform_enqueued_jobs
expect(ActionMailer::Base.deliveries.count).to eq(2)
english_mail = ActionMailer::Base.deliveries.find { |m| m.to.include?(english_recipient.mail) }
german_mail = ActionMailer::Base.deliveries.find { |m| m.to.include?(german_recipient.mail) }
expect(english_mail.html_part.body).to include("Every day")
expect(english_mail.html_part.body).not_to include("Jeden Tag")
expect(german_mail.html_part.body).to include("Jeden Tag")
expect(german_mail.html_part.body).not_to include("Every day")
end
end
end
describe "rescheduling occurrences" do
@@ -44,7 +44,8 @@ module OpenIDConnect
),
disabled: provider.seeded_from_env?,
required: false,
input_width: :large
input_width: :large,
value: pretty_claims
)
f.text_field(
@@ -59,6 +60,15 @@ module OpenIDConnect
input_width: :large
)
end
private
def pretty_claims
claims = model.claims || ""
JSON.pretty_generate(JSON.parse(claims))
rescue JSON::ParserError
claims
end
end
end
end
@@ -32,6 +32,8 @@ module OpenIDConnect
class BaseContract < ModelContract
include RequiresAdminGuard
VALID_CLAIMS_KEYS = %w[id_token userinfo].freeze
def self.model
OpenIDConnect::Provider
end
@@ -81,11 +83,45 @@ module OpenIDConnect
def claims_are_json
return if claims.blank?
JSON.parse(claims)
parsed = JSON.parse(claims)
return errors.add(:claims, :not_json_object) unless parsed.is_a?(Hash)
validate_claims_json_structure(parsed)
rescue JSON::ParserError
errors.add(:claims, :not_json)
end
def validate_claims_json_structure(parsed)
invalid_keys = parsed.keys - VALID_CLAIMS_KEYS
if invalid_keys.any?
return errors.add(:claims,
:invalid_claims_location,
invalid: invalid_keys.join(", "),
supported: VALID_CLAIMS_KEYS.join(", "))
end
non_object_key, = parsed.find { |_, v| !v.is_a?(Hash) }
return errors.add(:claims, :non_object_attribute, attribute: non_object_key) if non_object_key
parsed.each_key do |key|
validate_nested_claims_structure(parsed, key)
end
end
def validate_nested_claims_structure(parsed, base_key)
claims = parsed.fetch(base_key)
claims.each do |key, value|
next if value.nil?
return errors.add(:claims, :non_object_attribute, attribute: json_path(base_key, key)) unless value.is_a?(Hash)
if key_violates_type?(value, "essential", TrueClass, FalseClass)
return errors.add(:claims, :invalid_claims_essential, attribute: json_path(base_key, key, "essential"))
end
if key_violates_type?(value, "values", Array)
return errors.add(:claims, :invalid_claims_values, attribute: json_path(base_key, key, "values"))
end
end
end
def group_regexes_parseable
invalid_lines = group_regexes.each_with_index.filter_map do |r, i|
Regexp.new(r)
@@ -96,6 +132,16 @@ module OpenIDConnect
errors.add(:group_regexes, :regex_list_invalid, invalid_lines: invalid_lines.to_sentence) if invalid_lines.any?
end
def json_path(*elements)
elements.join(".")
end
def key_violates_type?(object, key, *types)
return false unless object.key?(key)
types.all? { |t| !object.fetch(key).is_a?(t) }
end
end
end
end
@@ -44,6 +44,10 @@ en:
response_is_not_successful: " responds with %{status}."
response_is_not_json: " does not return JSON body."
response_misses_required_attributes: " does not return required attributes. Missing attributes are: %{missing_attributes}."
invalid_claims_essential: "does not define a boolean at %{attribute}."
invalid_claims_location: "contain unsupported locations: %{invalid}. Supported locations are: %{supported}."
invalid_claims_values: "does not define an array at %{attribute}."
non_object_attribute: "does not define a JSON object at %{attribute}."
provider:
delete_warning:
@@ -53,6 +53,74 @@ RSpec.describe OpenIDConnect::Providers::UpdateContract do
it_behaves_like "contract is valid"
end
end
describe "claims" do
let(:provider) { build_stubbed(:oidc_provider, claims:) }
let(:claims) { claims_object.to_json }
let(:claims_object) { { id_token: { my_claim: { essential: true } } } }
it_behaves_like "contract is valid"
context "when claims are empty" do
let(:claims) { "" }
it_behaves_like "contract is valid"
end
context "when a voluntary claim without special needs is requested" do
let(:claims_object) { { id_token: { my_claim: nil } } }
it_behaves_like "contract is valid"
end
context "when an essential claim with specific values is requested" do
let(:claims_object) { { id_token: { my_claim: { essential: true, values: %w[a b] } } } }
it_behaves_like "contract is valid"
end
context "when claims are not JSON" do
let(:claims) { "foobar" }
it_behaves_like "contract is invalid", claims: :not_json
end
context "when claims are not a JSON object" do
let(:claims_object) { "a JSON string" }
it_behaves_like "contract is invalid", claims: :not_json_object
end
context "when claims contain unsupported root key" do
let(:claims_object) { { something: { my_claim: nil } } }
it_behaves_like "contract is invalid", claims: :invalid_claims_location
end
context "when claims don't contain object at root key" do
let(:claims_object) { { id_token: "not allowed to be a string" } }
it_behaves_like "contract is invalid", claims: :non_object_attribute
end
context "when definition of a claim is not a hash" do
let(:claims_object) { { id_token: { my_claim: "essential" } } }
it_behaves_like "contract is invalid", claims: :non_object_attribute
end
context "when essential attribute of a claim is not a boolean" do
let(:claims_object) { { id_token: { my_claim: { essential: "false" } } } }
it_behaves_like "contract is invalid", claims: :invalid_claims_essential
end
context "when values attribute of a claim is not an array" do
let(:claims_object) { { id_token: { my_claim: { values: "foobar" } } } }
it_behaves_like "contract is invalid", claims: :invalid_claims_values
end
end
end
context "when non-admin" do
@@ -70,7 +70,7 @@ ro:
label_filter: "Filtrează"
label_filter_add: "Adaugă filtru"
label_filter_plural: "Filtre"
label_group_by: "Grupează după"
label_group_by: "Grupare după"
label_group_by_add: "Adaugă atributul Grupează-după"
label_inactive: "Inactiv"
label_no: "Nu"
@@ -53,7 +53,7 @@ zh-TW:
label_money: "金額"
label_month_reporting: "月"
label_new_report: "新建成本報表"
label_open: "開啟"
label_open: "開啟"
label_operator: "操作員"
label_private_report_plural: "私密成本報告"
label_progress_bar_explanation: "產生報告中..."
@@ -70,7 +70,7 @@ zh-TW:
label_filter: "篩選條件"
label_filter_add: "新增篩選條件"
label_filter_plural: "篩選條件"
label_group_by: "分"
label_group_by: "分組依據"
label_group_by_add: "新增群組欄位"
label_inactive: "«不活動»"
label_no: "否"
@@ -131,14 +131,17 @@ class OpenProject::Reporting::CostEntryXlsTable < OpenProject::XlsExport::XlsVie
%i[user_id activity_id entity_gid comments project_id]
end
# Returns the results of the query sorted by date the time was spent on and by id
# Returns the results of the query sorted by date the time was spent on and name
def sorted_results
query
.each_direct_result
.map(&:itself)
results = query.each_direct_result.map(&:itself)
users_by_id = load_users_for_results(results)
results
.group_by { |r| r.fields["spent_on"] }
.sort
.flat_map { |_, date_results| date_results.sort_by { |r| r.fields["id"] } }
.flat_map do |_, date_results|
date_results.sort_by { |r| user_name_for_sorting(r, users_by_id) }
end
end
def labour_query?
@@ -148,4 +151,13 @@ class OpenProject::Reporting::CostEntryXlsTable < OpenProject::XlsExport::XlsVie
def with_times_column?
Setting.allow_tracking_start_and_end_times && labour_query?
end
def load_users_for_results(results)
user_ids = results.map { |r| r.fields["user_id"] }.uniq
User.where(id: user_ids).index_by(&:id)
end
def user_name_for_sorting(result, users_by_id)
users_by_id[result.fields["user_id"]]&.name&.downcase || ""
end
end
@@ -30,25 +30,29 @@ See COPYRIGHT and LICENSE files for more details.
<%=
render(
Primer::OpenProject::DangerDialog.new(
id:,
title:,
test_selector: id,
title: I18n.t("storages.page_titles.file_storages.delete"),
form_arguments:,
size: :large,
form_arguments: {
model: @project_storage,
action: admin_settings_storage_project_storage_path(id: @project_storage, page: current_page),
data: {
turbo: true
},
method: :delete
},
confirm_button_text: I18n.t("button_remove")
test_selector: TEST_SELECTOR,
confirm_button_text: I18n.t("button_remove_permanently")
)
) do |dialog|
dialog.with_confirmation_message do |message|
message.with_heading(tag: :h2) { heading }
message.with_description_content(text)
end
dialog.with_confirmation_check_box_content(confirmation_text)
end
%>
<% dialog.with_confirmation_message do |message|
message.with_heading(tag: :h2) { I18n.t("storages.page_titles.project_storages.delete") }
end %>
<% dialog.with_additional_details do %>
<% render(Primer::OpenProject::FlexLayout.new) do |flex| %>
<% flex.with_row do %>
<%= t("storages.delete_warning.project_storage", file_storage: "<strong>#{h(@project_storage.storage.name)}</strong>").html_safe %>
<% end %>
<% flex.with_row do %>
<ul>
<li> <%= t("storages.delete_warning.project_storage_delete_result_1") %>
<li> <%= t("storages.delete_warning.project_storage_delete_result_2") %>
</ul>
<% end %>
<% end %>
<% end %>
<% dialog.with_confirmation_check_box_content(I18n.t(:text_permanent_remove_confirmation_checkbox_label)) %>
<% end %>
@@ -1,6 +1,6 @@
# frozen_string_literal: true
#-- copyright
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
@@ -26,49 +26,45 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
# ++
# This components renders a dialog to confirm the deletion of a project from a storage.
module Storages::ProjectStorages
class DestroyConfirmationDialogComponent < ApplicationComponent
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
# Destroy confirmation dialog used when removing a storage from a project from within the project
# by going to "Some project" -> Project settings -> Files.
module Storages
module ProjectStorages
class DestroyConfirmationDialogComponent < ApplicationComponent
include OpTurbo::Streamable
def initialize(storage:, project_storage:, params: {})
super
TEST_SELECTOR = "op-project-storages--delete-dialog"
@storage = storage
@project_storage = project_storage
@params = params
end
def id
"project-storage-#{@project_storage.id}-destroy-confirmation-dialog"
end
def title
I18n.t("project_storages.remove_project.dialog.title")
end
def heading
I18n.t("project_storages.remove_project.dialog.heading_text", storage: @storage.name)
end
def text
text = I18n.t("project_storages.remove_project.dialog.text")
if @project_storage.project_folder_mode == "automatic"
text << " "
text << I18n.t("project_storages.remove_project.dialog.automatically_managed_appendix")
# @param target [Symbol] The submission target of the dialog's form. One of :project or :storage
# @param target_page [String, Integer] An optional page query parameter for the target that the form will submit to.
def initialize(project_storage:, target:, target_page: nil)
super
@project_storage = project_storage
@target = target
@target_page = target_page
end
text
end
def current_page
@params[:page]
end
private
def confirmation_text
I18n.t("project_storages.remove_project.dialog.confirmation_text")
def form_arguments
{
action: target_path,
method: :delete
}
end
def target_path
case @target
when :project
project_settings_project_storage_path(project_id: @project_storage.project, id: @project_storage, page: @target_page)
when :storage
admin_settings_storage_project_storage_path(id: @project_storage, page: @target_page)
else
raise ArgumentError, "Unsupported target #{@target} for #{self.class}"
end
end
end
end
end
@@ -1,57 +0,0 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<%=
render(
Primer::OpenProject::DangerDialog.new(
title: I18n.t("storages.page_titles.file_storages.delete"),
form_arguments:,
size: :large,
test_selector: TEST_SELECTOR
)
) do |dialog|
%>
<% dialog.with_confirmation_message do |message|
message.with_heading(tag: :h2) { I18n.t("storages.page_titles.file_storages.delete") }
end %>
<% dialog.with_additional_details do %>
<% render(Primer::OpenProject::FlexLayout.new) do |flex| %>
<% flex.with_row do %>
<%= t("storages.delete_warning.storage", file_storage: "<strong>#{h(@project_storage.storage.name)}</strong>").html_safe %>
<% end %>
<% flex.with_row do %>
<ul>
<li> <%= t("storages.delete_warning.project_storage_delete_result_1") %>
<li> <%= t("storages.delete_warning.project_storage_delete_result_2") %>
</ul>
<% end %>
<% end %>
<% end %>
<% dialog.with_confirmation_check_box_content(I18n.t(:text_permanent_delete_confirmation_checkbox_label)) %>
<% end %>
@@ -1,55 +0,0 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
# Destroy confirmation dialog used when removing a storage from a project from within the project
# by going to "Some project" -> Project settings -> Files.
module Storages
module ProjectStorages
module Projects
class DestroyConfirmationDialogComponent < ApplicationComponent
include OpTurbo::Streamable
TEST_SELECTOR = "op-project-storages--delete-dialog"
def initialize(storage:)
super
@project_storage = storage
end
def form_arguments
{
action: project_settings_project_storage_path(project_id: @project_storage.project, id: @project_storage),
method: :delete
}
end
end
end
end
end
@@ -128,7 +128,10 @@ class Storages::Admin::ProjectStoragesController < Projects::SettingsController
end
def destroy_info
respond_with_dialog Storages::ProjectStorages::Projects::DestroyConfirmationDialogComponent.new(storage: @object)
respond_with_dialog Storages::ProjectStorages::DestroyConfirmationDialogComponent.new(
project_storage: @object,
target: :project
)
end
private
@@ -115,9 +115,9 @@ class Storages::Admin::Storages::ProjectStoragesController < ApplicationControll
def destroy_confirmation_dialog
respond_with_dialog Storages::ProjectStorages::DestroyConfirmationDialogComponent.new(
storage: @storage,
project_storage: @project_storage,
params:
target: :storage,
target_page: params[:page]
)
end
+106 -106
View File
@@ -18,7 +18,7 @@ ja:
token_exchange_scope: ストレージスコープ
storages/project_storage:
project_folder: プロジェクトフォルダ
project_folder_mode: プロジェクトフォルダモード
project_folder_mode: プロジェクトフォルダモード
storage: ストレージ
storage_url: ストレージURL
storages/sharepoint_storage:
@@ -29,53 +29,53 @@ ja:
storages/storage:
authentication_method: 認証方法
creator: 作成者
drive: ドライブID
drive: ドライブ ID
host: ホスト
name: 名称
password: アプリケーションのパスワード
provider_type: プロバイダー・タイプ
tenant: ディレクトリテナントID
provider_type: プロバイダーの種類
tenant: ディレクトリ (テナント) ID
errors:
messages:
invalid_host_url: は有効な URL ではありません。
invalid_sharepoint_url: は有効なSharePointサイト、ライブラリ、ドキュメントのURLではありません。
not_linked_to_project: はプロジェクトにリンクされていない
invalid_sharepoint_url: は有効なSharePointサイト、ライブラリ、またはドキュメントのURLではありません。
not_linked_to_project: はプロジェクトにリンクされていません
models:
storages/file_link:
attributes:
origin_id:
only_numeric_or_uuid: は数値uuidしか指定できない
only_numeric_or_uuid: は数値またはuuidのみとなります
storages/project_storage:
attributes:
project_folder_id:
blank: フォルダーを選択してください。
project_folder_mode:
mode_unavailable: このストレージでは使用できない
mode_unavailable: このストレージでは使用できません
project_ids:
blank: プロジェクトを選択してください。
storages/storage:
attributes:
host:
authorization_header_missing: 完全にセットアップされていません。APIリクエストのベアラートークンベースの認に必要な "Authorization "ヘッダーをNextcloudインスタンスが受け取っていません。HTTPサーバーの設定を再度ご確認ください。
cannot_be_connected_to: に到達できませんでした。ホスト到達可能で、OpenProject 統合アプリがインストールされていることを確認してください。
minimal_nextcloud_version_unmet: 最小バージョン要件を満たしていないNextcloud 23以上である必要があります。
not_nextcloud_server: はNextcloudサーバーではありません
op_application_not_installed: は、アプリ「OpenProject integration」がインストールされていないようです。インストールしてからもう一度お試しください。
authorization_header_missing: 完全には設定されていません。 Nextcloudインスタンスは、APIリクエストのベアラートークンベースの認に必要なAuthorizationヘッダーを受け取りません。 HTTPサーバーの設定を再確認してください。
cannot_be_connected_to: に到達できませんでした。ホスト到達可能で、OpenProject 統合アプリがインストールされていることを確認してください。
minimal_nextcloud_version_unmet: 最小バージョン要件を満たしていませんNextcloud23以上でなければなりません
not_nextcloud_server: はNextcloudサーバーではありません
op_application_not_installed: アプリ「OpenProject統合」がインストールされていません。最初にインストールしてからもう一度お試しください。
password:
invalid_password: は無効である
invalid_password: は無効で
unknown_error: could not be validated with the file storage provider. Please verify that the connection is functioning properly.
models:
file_link: ファイル
storages/storage: ストレージ
api_v3:
errors:
too_many_elements_created_at_once: 一度に作成され要素が多すぎる。最大でも %{max} %{actual}。
too_many_elements_created_at_once: 一度に作成され要素が多すぎます。 %{max} の期待値は %{actual} です
external_file_storages: 外部ファイルストレージ
permission_create_files: '自動的に管理されたプロジェクトフォルダ: ファイルの作成'
permission_create_files_explanation: この権限はNextcloudストレージでのみ利用できます
permission_delete_files: '自動的に管理されたプロジェクトフォルダ: ファイルの削除'
permission_delete_files_explanation: この権限はNextcloudストレージでのみ利用できます
permission_header_for_project_module_storages: 自動的に管理されプロジェクトフォルダ
permission_header_for_project_module_storages: 自動的に管理されプロジェクトフォルダ
permission_manage_file_links: ファイルへのリンク管理
permission_manage_files_in_project: プロジェクト内のファイル管理
permission_read_files: '自動的に管理されたプロジェクトフォルダ: ファイルの読み込み'
@@ -86,21 +86,21 @@ ja:
project_module_storages: ファイルを添付する
project_storages:
edit_project_folder:
label: プロジェクトフォルダ編集
label: プロジェクトフォルダ編集
open:
contact_admin: このエラーを解決するには管理者に連絡してください。
remote_identity_error: ストレージへの接続中に予期せぬエラーが発生しました。
contact_admin: このエラーを解決するには管理者に問い合わせてください。
remote_identity_error: ストレージへの接続中に予期しないエラーが発生しました。
project_folder_mode:
automatic: 自動的に管理される
inactive: 特定のフォルダなし
automatic: 自動的に管理
inactive: 特定のフォルダがありません
manual: 既存のフォルダを手動で管理
remove_project:
deletion_failure_flash: ストレージからのプロジェクトの削除に失敗しました。 %{error}
deletion_failure_flash: プロジェクトをストレージから削除できませんでした。 %{error}
dialog:
automatically_managed_appendix: また、この場合、このストレージには自動的に管理されプロジェクトフォルダがあり、このフォルダとそのファイルは永久に削除され
automatically_managed_appendix: また、この場合、このストレージには自動的に管理されプロジェクトフォルダがあり、そのファイルは永久に削除されます
confirmation_text: このプロジェクトからこのファイルストレージを削除したいことを確認してください
heading_text: '%{storage}からプロジェクトを削除する'
text: このアクションは不可逆的で、このプロジェクトのワークパッケージからそのストレージのファイルやフォルダへのリンクをすべて削除します。
heading_text: '%{storage} からプロジェクトを削除'
text: この操作は元に戻すことができず、このプロジェクトの作業パッケージからそのストレージのファイルやフォルダへのすべてのリンクを削除します。
title: ストレージからプロジェクトを削除
label: プロジェクトを削除
services:
@@ -116,7 +116,7 @@ ja:
one_drive_sync_service:
create_folder: 'プロジェクトフォルダの作成を管理:'
ensure_root_folder_permissions: 'ベースフォルダの権限を設定:'
hide_inactive_folders: '非アクティブフォルダを隠す ステップ'
hide_inactive_folders: '非アクティブフォルダを隠す ステップ'
remote_folders: 'グループフォルダの内容を読む:'
rename_project_folder: '管理プロジェクトフォルダの名前を変更します:'
sharepoint_sync_service:
@@ -127,15 +127,15 @@ ja:
rename_project_folder: '管理プロジェクトフォルダの名前を変更します:'
errors:
messages:
error: 予期しないエラーが発生しました。OpenProject のログを確認するか、管理者に連絡してください
error: 予期しないエラーが発生しました。OpenProject のログを確認するか、管理者に連絡してください
unauthorized: OpenProjectはストレージプロバイダと認証できませんでした。アクセスできることを確認してください。
models:
copy_project_folders_service:
conflict: フォルダ %{destination_path} は既に存在する。上書きを避けるために処理を中断しています。
error: 予期しないエラーが発生しました。OpenProject のログを確認するか、管理者に連絡してください
forbidden: OpenProject はソースフォルダにアクセスできませんでした。ストレージ・プロバイダの権限設定を確認してください
error: 予期しないエラーが発生しました。OpenProject のログを確認するか、管理者に連絡してください
forbidden: OpenProject はソースフォルダにアクセスできませんでした。ストレージ・プロバイダの権限設定を確認してください
not_found: ソース・テンプレートの場所 %{source_path} が見つかりませんでした。
unauthorized: OpenProject はストレージプロバイダと認証できませんでした。ストレージの設定を確認してください
unauthorized: OpenProject はストレージプロバイダと認証できませんでした。ストレージの設定を確認してください
nextcloud_sync_service:
attributes:
add_user_to_group:
@@ -156,31 +156,31 @@ ja:
conflict: '以下の理由により、 %{user} のユーザーを %{group} グループから削除できませんでした: %{reason}'
failed_to_remove: '以下の理由により、 %{user} のユーザーを %{group} グループから削除できませんでした: %{reason}'
rename_project_folder:
conflict: OpenProjectは、同じ名前のフォルダが既に存在するため、プロジェクトフォルダの名前を %{current_path} に変更できませんでした
forbidden: OpenProject ユーザーは %{current_path} フォルダにアクセスできません。
not_found: "%{current_path} は見つからなかった。"
conflict: OpenProjectは、同じ名前のフォルダが既に存在するため、プロジェクトフォルダの名前を %{current_path} に変更できませんでした
forbidden: OpenProjectユーザーは %{current_path} フォルダにアクセスできません。
not_found: "%{current_path} は見つかりませんでした。"
set_folders_permissions:
permission_not_set: '%{path}にパーミッションを設定できなかった。'
error: 予期しないエラーが発生しました。Nextcloud インスタンスに到達可能であることを確認し、OpenProject ワーカーログを確認してください
permission_not_set: '%{path} に権限を設定できませんでした。'
error: 予期しないエラーが発生しました。Nextcloudインスタンスがアクセス可能であることを確認し、詳細についてはOpenProjectワーカーログを確認してください
group_does_not_exist: "%{group} は存在しません。Nextcloudインスタンスの設定を確認してください。"
insufficient_privileges: OpenProjectには、 %{group}に %{user} を追加するの十分な権限がありません。Nextcloudグループ設定を確認してください。
not_allowed: ネクストクラウドはリクエストをブロックす
insufficient_privileges: OpenProjectには %{user} を %{group}に追加するための十分な権限がありません。Nextcloudグループ設定を確認してください。
not_allowed: Nextcloudはリクエストをブロックします。
not_found: OpenProject could not find the file on the Nextcloud Storage Provider. Please check if it wasn't deleted.
unauthorized: OpenProjectがNextcloudと同期できませんでした。ストレージとNextcloudの設定を確認してください。
user_does_not_exist: "%{user} はNextcloudには存在しません。"
user_does_not_exist: "Nextcloudには%{user} は存在しません。"
one_drive_sync_service:
attributes:
create_folder:
conflict: '%{folder_name} はすでに %{parent_location}に存在している。'
not_found: "%{parent_location} は見つからなかった。"
conflict: '%{folder_name} は %{parent_location} に既に存在します。'
not_found: "%{parent_location} は見つかりませんでした。"
hide_inactive_folders:
permission_not_set: '%{path}にパーミッションを設定できなかった。'
permission_not_set: '%{path} に権限を設定できませんでした。'
remote_folders:
request_error: OpenProject %{drive_id}ドライブにアクセスできませんでした。ストレージの設定が正しいかどうか確認してください。
request_error: OpenProjectがドライブ %{drive_id}にアクセスできませんでした。ストレージの設定が正しいか確認してください。
rename_project_folder:
conflict: OpenProject could not rename the folder %{current_path} to %{project_folder_name} as a folder with the same name already exists.
forbidden: OpenProject は %{current_path} にアクセスできず、名前を変更できません。
not_found: "%{current_path} は見つからなかった。"
forbidden: OpenProject は名前を変更するために %{current_path} にアクセスできません。
not_found: "%{current_path} は見つかりませんでした。"
set_folders_permissions:
permission_not_set: '%{path} に権限を設定できませんでした。'
error: An unexpected error occurred. Please ensure that OneDrive is reachable and check OpenProject worker logs for more information.
@@ -301,41 +301,41 @@ ja:
drive_id_format: ドライブIDフォーマット
header: 構成
host: ホスト URL
host_url_accessible: アクセス可能なホストURL
host_url_accessible: ホスト URL アクセス
storage_configured: 設定完了
tenant_id: テナントID
tenant_id: Tenant ID
failures:
other: "%{count} チェック失敗"
other: "%{count} チェック失敗しました"
success: すべてのチェックに合格
warnings:
other: "%{count} は警告を返しました"
connection_validation:
client_id_invalid: 設定されたOAuth 2クライアントIDが無効です。設定を確認してください。
client_secret_invalid: 設定されたOAuth 2クライアントシークレットが無効です。設定を確認してください。
nc_dependency_missing: 'ファイルストレージに必要な依存関係がありません。次の依存関係を追加してください %{dependency}。'
nc_dependency_missing: 'ファイルストレージに必要な依存関係がありません。次の依存関係を追加してください: %{dependency}。'
nc_dependency_version_mismatch: '%{dependency} アプリのバージョンがサポートされていません。Nextcloudサーバーをアップデートしてください。'
nc_group_folder_not_found: グループフォルダが見つかりません。
nc_host_not_found: 設定されたホストURLにNextcloudサーバーが見つかりません。設定を確認してください。
nc_oauth_request_not_found: 現在接続しているユーザーを取得するエンドポイントが見つかりませんでした。詳細については、サーバーのログを確認してください。
nc_oauth_request_unauthorized: 現在のユーザーにはリモートファイルストレージにアクセスする権限がありません。サーバーのログを確認してください。
nc_oauth_token_missing: OpenProjectは、ユーザーがNextcloudアカウントリンクしていないため、ユーザーレベルのNextcloudとの通信をテストできません。
nc_unexpected_content: 管理グループフォルダに予期しないコンテンツが見つかりました。
nc_oauth_token_missing: OpenProject は、Nextcloudアカウントへのリンクがまだないため、Nextcloudとのユーザーレベルの通信をテストできません。
nc_unexpected_content: 管理対象グループフォルダに予期しないコンテンツが見つかりました。
nc_userless_access_denied: 設定されているアプリのパスワードが無効です。
not_configured: 接続を検証できませんでした。先に設定を完了してください。
od_client_cant_delete_folder: クライアントがフォルダ削除に失敗しています。お使いのストレージのセットアップドキュメントを確認してください。
od_client_write_permission_missing: クライアント書き込み権限が不足しているようです。お使いのストレージのセットアップドキュメントを確認してください。
od_drive_id_invalid: 設定されたドライブIDが無効のようです。設定を確認してください。
od_drive_id_not_found: 設定されたドライブIDが見つかりません。設定を確認してください。
od_oauth_request_not_found: 現在接続ているユーザーを取得するエンドポイントが見つかりませんでした。詳細についてはサーバーログを確認してください。
od_oauth_request_unauthorized: 現在のユーザーはリモートファイルストレージにアクセスする権限がありません。サーバーログを確認してください。
od_oauth_token_missing: OpenProjectは、ユーザーがまだMicrosoftアカウントをリンクしていないため、OneDriveとのユーザーレベルの通信をテストできません。
od_tenant_id_wrong: 設定されたディレクトリテナントID無効です。設定を確認してください。
od_client_cant_delete_folder: クライアントがフォルダ削除できません。ストレージのセットアップドキュメントを確認してください。
od_client_write_permission_missing: クライアント書き込み権限がありません。ストレージの設定ドキュメントを確認してください。
od_drive_id_invalid: 設定されたドライブ ID が無効です。設定を確認してください。
od_drive_id_not_found: 設定されたドライブ ID が見つかりません。設定を確認してください。
od_oauth_request_not_found: 現在接続されているユーザーを取得するエンドポイントが見つかりませんでした。詳細についてはサーバーログを確認してください。
od_oauth_request_unauthorized: 現在のユーザーはリモートファイルストレージにアクセスする権限がありません。詳細についてはサーバーログを確認してください。
od_oauth_token_missing: OpenProject は、ユーザーが Microsoft アカウントをまだリンクしていないため、OneDrive とのユーザー レベルの通信をテストできません。
od_tenant_id_wrong: 設定されたディレクトリ (テナント) ID無効です。設定を確認してください。
od_test_folder_exists: テストに必要なフォルダ %{folder_name} はすでに存在します。削除して再度お試しください。
od_unexpected_content: ドライブに予期しないコンテンツが見つかりました。
offline_access_scope_missing: OpenID Connectプロバイダがoffline_accessスコープを要求するように設定することをお勧めします。統合はまだ機能するかもしれませんが、リフレッシュトークンの有効期限切れていないことを確認してください。
offline_access_scope_missing: offline_access スコープを要求するために OpenID Connect プロバイダを設定することをお勧めします。 統合はまだ動作するかもしれませんが、更新トークン期限切れないことを確認してください。
oidc_cant_refresh_token: ストレージへのアクセスを確認中にエラーが発生しました。詳細についてはサーバーログを確認してください。
oidc_non_oidc_user: 現在のユーザはプロビジョニングされていますが、OpenID Connect (OIDC) Identity Providerによってプロビジョニングされていません。OIDCプロビジョニングされたユーザでチェックを再実行してください。
oidc_non_provisioned_user: 現在のユーザはOpenID Connect Identity Providerから提供されていません。提供されたユーザーチェックを再実行してください。
oidc_non_oidc_user: 現在のユーザはプロビジョニング中にOpenID Connect(OIDC)アイデンティティプロバイダによってプロビジョニングされていませんでした。OIDCプロビジョニングされたユーザでチェックを再実行してください。
oidc_non_provisioned_user: 現在のユーザはOpenID Connectアイデンティティプロバイダーによって提供されていません。指定されたユーザーチェックを再実行してください。
oidc_provider_cant_exchange: OpenID Connectプロバイダはトークン交換をサポートしていないようですが、トークン交換はストレージ用に設定されています。
oidc_token_acquisition_failed: OpenID Connectのセットアップでは、必要なオーディエンスが提供されておらず、トークン交換機能も提供されていません。詳しくはドキュメントをご覧ください。
oidc_token_exchange_failed: OpenID Connect ProviderのToken Exchange設定に問題があるようです。設定を確認し、再度お試しください。
@@ -350,7 +350,7 @@ ja:
sp_oauth_token_missing: OpenProject は、ユーザーがまだ SharePoint アカウントをリンクしていないため、ユーザーレベルの SharePoint との通信をテストできません。
sp_tenant_id_missing: 構成されたディレクトリ(テナント)IDがSharePointにありません。設定を確認してください。
sp_unexpected_content: Unexpected content found in the SharePoint Document Library.
unknown_error: 接続を検証できませんでした。不明なエラーが発生しました。詳細についてはサーバーログを確認してください。
unknown_error: 接続を検証できませんでした。不明なエラーが発生しました。詳細についてはサーバーログを確認してください。
label_error: エラー
label_failed: 失敗しました
label_healthy: 健康的
@@ -358,55 +358,55 @@ ja:
label_pending: 保留中
label_skipped: スキップ
label_warning: 注意
no_report: 報告書なし
no_report_description: 今すぐチェックを実行し、このファイルストレージの完全な健全性ステータスをレポートする
no_report: 利用可能なレポートがありません
no_report_description: 今すぐこのファイルストレージの完全な健康状態レポートを確認します
open_report: 完全な健康報告を開く
project_folders:
subtitle: 自動的に管理されるプロジェクトフォルダ
since: '%{datetime}より'
since: '%{datetime} 以降'
summary:
failure: いくつかのチェックに失敗し、システムが期待通りに機能しない
success: すべての接続とシステムは期待通りに機能してい
warning: いくつかのチェック警告を返した。これは予期せぬ動作につながる可能性があ
title: 健康状態報告
failure: いくつかのチェックに失敗し、システムが期待どおりに動作しません
success: すべての接続とシステムは期待どおりに動作しています
warning: いくつかのチェック警告を返しました。これは予期しない動作につながる可能性があります
title: 健康状態レポート
health_email_notifications:
description_disabled: 管理者は、重要なアップデートがあった場合、メールでアップデートを受け取ることはできません。
description_enabled: 管理者は、重要なアップデートがあった場合、メールで最新情報を受け取ります。
error_could_not_be_saved: 電子メール通知設定を保存できませんでした。もう一度お試しください。
error_could_not_be_saved: メール通知設定を保存できませんでした。もう一度やり直してください。
title: 管理者にメールで更新する
help_texts:
project_folder: プロジェクトフォルダは、このプロジェクトのファイルアップロードのデフォルトフォルダです。それでも、ユーザーは他の場所にファイルをアップロードすることができます。
project_folder_bulk: プロジェクトフォルダは、選択したすべてのプロジェクトのファイルアップロードのデフォルトフォルダです。これは、各プロジェクト設定で個別に変更できます。それでも、ユーザーはの場所にファイルをアップロードすることできます。
project_folder: プロジェクトフォルダは、このプロジェクトのファイルアップロードのデフォルトフォルダです。ただし、ユーザーは他の場所にファイルをアップロードすることができます。
project_folder_bulk: プロジェクトフォルダは、選択したすべてのプロジェクトのファイルアップロードのデフォルトフォルダです。 プロジェクトごとの設定で個別に変更することができますが、ユーザーはの場所にファイルをアップロードすることできます。
instructions:
all_available_storages_already_added: 利用可能なすべてのストレージはすでにプロジェクトに追加されてい
authentication_method: OpenProject とストレージ間のリクエスト認証方法
automatic_folder: これにより、このプロジェクトのルートフォルダが自動的に作成され、各プロジェクトメンバーのアクセス権が管理されます。
empty_project_folder_validation: 続行するには、フォルダの選択必須です。
existing_manual_folder: 既存のフォルダをこのプロジェクトのルートフォルダとして指定することができます。ただし、パーミッションは自動的に管理されないため、管理者は関連するユーザーアクセスできることを手動で確認する必要があります。選択したフォルダは複数のプロジェクトで使用できます。
host: https:// を含むストレージのホストアドレスを追加してください。255文字以内にしてください。
managed_project_folders_application_password_caption: '%{provider_type_link}からこの値をコピーして、自動管理フォルダを有効にす。'
name: ユーザーが複数のストレージを区別できるように、ストレージに名前を付け
all_available_storages_already_added: 利用可能なすべてのストレージが既にプロジェクトに追加されています
authentication_method: OpenProjectとストレージ間のリクエスト認証されます
automatic_folder: これにより、このプロジェクトのルートフォルダが自動的に作成され、各プロジェクトメンバーのアクセス権が管理されます。
empty_project_folder_validation: フォルダの選択必須です。
existing_manual_folder: このプロジェクトのルートフォルダとして既存のフォルダを指定できます。 ただし、権限は自動的に管理されておらず、管理者は関連するユーザーに手動でアクセス権があることを確認する必要があります。 選択したフォルダは複数のプロジェクトで使用できます。
host: https://を含むストレージのホストアドレスを追加してください。255文字以内にしてください。
managed_project_folders_application_password_caption: '%{provider_type_link} からこの値をコピーすることで、自動管理フォルダを有効にします。'
name: ユーザーが複数のストレージを区別できるように、ストレージに名前を付けます
new_storage: 詳しくは、<a target='_blank' href='%{provider_link}'> %{provider_name} ファイルストレージ</a>統合の設定に関するドキュメントをお読みください。
nextcloud:
application_link_text: アプリケーション "Integration OpenProject"
integration: ネクストクラウド管理 / OpenProject
integration: Nextcloudの管理 / OpenProject
oauth_configuration: '%{application_link_text} からこれらの値をコピーします。'
provider_configuration: セットアップを行う前に、Nextcloudインスタンス管理権限があり、 %{application_link_text} がインストールされていることを確認してください。
storage_audience: Nextcloud インスタンスが ID プロバイダとの通信に使用するクライアント ID。
storage_audience_placeholder: 例:ネクストクラウド
token_exchange_scope: トークン交換に要求するスコープ、それぞれスペースで区切って指定する
no_specific_folder: デフォルトでは、ファイルをアップロードすると、各ユーザーは自分のホームフォルダから開始します。
no_storage_set_up: ファイルストレージはまだ設定されていない
not_logged_into_storage: プロジェクトフォルダを選択するには、まずログインしてください
provider_configuration: Nextcloudインスタンス管理権限があり、設定を行う前に %{application_link_text} がインストールされていることを確認してください。
storage_audience: NextcloudインスタンスがIDプロバイダーと通信するために使用するクライアントID。
storage_audience_placeholder: 例:nextcloud
token_exchange_scope: トークン交換に要求されるべきスコープ、それぞれスペースで区切られています
no_specific_folder: デフォルトでは、各ユーザーはファイルをアップロードしたときに自分のホームフォルダから開始します。
no_storage_set_up: まだ設定されているファイルストレージがありません
not_logged_into_storage: プロジェクトフォルダを選択するには、最初にログインしてください
oauth_application_details: クライアントシークレットの値は、このウィンドウを閉じた後は二度とアクセスできなくなります。これらの値を %{oauth_application_details_link}にコピーしてください。
oauth_application_details_link_text: NextcloudOpenProject統合設定
oauth_application_details_link_text: Nextcloud OpenProjectインテグレーション設定
one_drive:
application_link_text: Azure Portal
copy_redirect_uri: リダイレクトURIをコピーする
documentation_link_text: OneDriveファイルストレージのドキュメント
drive_id: '%{drive_id_link_text} の手順に従って、目的のドライブからIDをコピーしてください。'
integration: ワンドライブ
missing_client_id_for_redirect_uri: OAuthの値を入力してURIを生成してください
integration: OneDrive
missing_client_id_for_redirect_uri: OAuthの値を入力してURIを生成してください
oauth_client_redirect_uri: この値を「リダイレクト URIs」にある新しい Web リダイレクト URI にコピーしてください。
oauth_client_secret: Client 資格情報にアプリケーション クライアント シークレットがない場合は、新しいシークレットを作成してください。
oauth_configuration: '%{application_link_text}、目的のアプリケーションからこれらの値をコピーします。'
@@ -478,13 +478,13 @@ ja:
login_button_aria_label: '%{storage} にログイン'
login_button_label: "%{provider_type} ログイン"
project_settings:
description: プロジェクトフォルダにアクセスするには、 %{storage}にログインする必要があります。
description: プロジェクトフォルダにアクセスするには、 %{storage} にログインする必要があります。
requesting_access_to: '%{storage} へのアクセスをリクエストしています'
storage_admin:
description: このストレージにプロジェクトを追加するには、 %{provider_type}にログインする必要があります。ログインしてもう一度やり直してください。
open_project_storage_modal:
success:
subtitle: リダイレクトされます
subtitle: リダイレクトしています
title: 連携のセットアップが完了しました
timeout:
link_text: ファイルストレージセットアップの状態の状態
@@ -503,8 +503,8 @@ ja:
subtitle_short: OpenProjectにプロジェクトごとにフォルダを自動的に作成させます。
title: 自動的に管理されるプロジェクトフォルダ
project_settings:
edit: このプロジェクトのファイルストレージを編集する
members_connection_status: メンバーの接続状況
edit: このプロジェクトのファイルストレージを編集
members_connection_status: 会員の接続状況
new: このプロジェクトにファイルストレージを追加する
project_storage_members:
subtitle: 全プロジェクトメンバーのストレージ %{storage_name_link} の接続状態を確認する。
@@ -513,14 +513,14 @@ ja:
provider_types:
label: プロバイダー・タイプ
nextcloud:
label_oauth_client_id: NextcloudOAuthクライアントID
label_oauth_client_secret: NextcloudOAuthクライアントシークレット
label_oauth_client_id: Nextcloud OAuthクライアントID
label_oauth_client_secret: Nextcloud OAuth クライアントシークレット
name: ネクストクラウド
name_placeholder: 例:ネクストクラウド
one_drive:
label_oauth_client_id: Azure OAuthアプリケーションクライアントID
label_oauth_client_id: Azure OAuth アプリケーション (クライアント) ID
label_oauth_client_secret: Azure OAuth クライアントの秘密値
name: ワンドライブ
name: OneDrive
name_placeholder: '例: OneDrive'
sharepoint:
drive_description: OpenProject access-managed document library
@@ -530,18 +530,18 @@ ja:
name_placeholder: 例:シェアポイント
show_attachments_toggle:
description: このオプションを無効にすると、作業パッケージのファイルタブの添付ファイルリストが非表示になります。ワークパッケージの説明に添付されたファイルは、内部添付ファイルストレージにアップロードされます。
label: ワークパッケージファイルタブに添付ファイルを表示
label: ワークパッケージファイルタブに添付ファイルを表示
storage_audience:
documentation_intro: 以下のオプションと ID プロバイダの設定の詳細については、当社のドキュメントをお読みください。
documentation_intro: アイデンティティプロバイダの以下のオプションと設定については、当社のドキュメントをお読みください。
idp:
helptext: OpenProjectは、ストレージへのリクエストを認証するために、ログインにIDプロバイダから受け取ったアクセストークンを使用します。別のトークンを取得しようとすることはありません。
label: ユーザーログインに取得したアクセストークンを使用する
helptext: OpenProjectはログインにIDプロバイダーが受け取ったアクセストークンを使用して、ストレージへのリクエストを認証します。 別のトークンを取得しようとません。
label: ログインに取得したアクセストークンを使用する
manual:
helptext: OpenProjectは、指定されたオーディエンスのIDプロバイダとトークンを交換します。
helptext: OpenProject は、特定のオーディエンスの ID プロバイダとトークンを交換します。
label: Manually specify audience for which to exchange access token (Recommended)
storage_list_blank_slate:
description: ストレージを追加して、ここで見ることができる
heading: あなたはまだ倉庫を持っていない
description: ここにそれらを見るためにストレージを追加します
heading: まだストレージがありません
successful_storage_connection: ストレージが正常に接続されました! 使用する各プロジェクトの「プロジェクト」タブでストレージをアクティブにすることを忘れないでください。
upsell:
one_drive:
@@ -3,14 +3,14 @@ ja:
js:
storages:
authentication_error: "%{storageType} での認証に失敗しました"
link_files_in_storage: "リンクファイル %{storageType}"
link_existing_files: "既存のファイルをリンク"
upload_files: "ファイルのアップロード"
link_files_in_storage: "%{storageType}のファイルをリンクする"
link_existing_files: "既存のファイルをリンクする"
upload_files: "ログファイル"
drop_files: "ここにファイルをドロップして、 %{name} にアップロードします。"
drop_or_click_files: "ここにファイルをドロップするか、クリックして %{name} にアップロードします。"
login: "%{storageType} ログイン"
login_to: "%{storageType}にログイン"
no_connection: "%{storageType} 接続がありません"
no_connection: "%{storageType} 接続なし"
open_storage: "%{storageType} を開く"
select_location: "場所を選択"
choose_location: "場所を選ぶ"
@@ -24,7 +24,7 @@ ja:
authentication_error: "%{storageType} へのリクエストを認証できませんでした。これはエラーです。"
connection_error: >
%{storageType} の設定が一部機能していません。 %{storageType} 管理者にお問い合わせください。
live_data_error: "ファイル詳細の取得に失敗しました"
live_data_error: "ファイル詳細の取得エラー"
live_data_error_description: >
一部の %{storageType} データを取得できませんでした。このページを再読み込みするか、 %{storageType} 管理者にお問い合わせください。
no_file_links: "このワークパッケージにファイルをリンクするには、 %{storageType}を使用してください。"
@@ -33,7 +33,7 @@ ja:
suggest_logout: ログアウトしてログインし直すと、この問題が解決するかどうか試してみてください。
suggest_relink: 以下のログインボタンからアカウントを再リンクすると、この問題が解決するかどうか試してみてください。
files:
already_existing_header: "このファイルはすでに存在す"
already_existing_header: "このファイルはに存在します"
already_existing_body: >
このファイルをアップロードしようとしている場所に、"%{fileName}"という名前のファイルがすでに存在します。どうしますか?
directory_not_writeable: "このフォルダにファイルを追加する権限がありません。"
@@ -41,7 +41,7 @@ ja:
dragging_folder: "%{storageType} へのアップロードはフォルダをサポートしていません。"
empty_folder: "このフォルダは空です。"
empty_folder_location_hint: "下のボタンをクリックして、この場所にファイルをアップロードしてください。"
file_not_selectable_location: "場所を選択する過程でファイルを選択することはできない。"
file_not_selectable_location: "ファイルを選択することは、場所を選択する過程ではできません。"
project_folder_no_access: >
プロジェクトフォルダにアクセスできません。管理者に連絡してアクセス権を取得するか、別の場所にファイルをアップロードしてください。
managed_project_folder_not_available: >
@@ -79,9 +79,9 @@ ja:
ファイル (%{fileName}) の容量がストレージ・クォータの許容量を超えています。管理者に連絡して、このクォータを変更してください。
detail:
nextcloud: >
最新版のNextcloudアプリ「OpenProject Integration」がインストールされていることを確認し、管理者にお問い合わせください。
Nextcloudアプリ「OpenProject統合」の最新バージョンがインストールされていることを確認し、詳細については管理者にお問い合わせください。
link_uploaded_file_error: >
最近アップロードされたファイル '%{fileName}' をワークパッケージ %{workPackageId}にリンクするエラーが発生しました。
最近アップロードされたファイル '%{fileName}' をワークパッケージ %{workPackageId} にリンクしてエラーが発生しました。
tooltip:
not_logged_in: "このファイルにアクセスするには、ストレージにログインしてください。"
view_not_allowed: "このファイルを閲覧する権限がありません。"
+10 -13
View File
@@ -97,12 +97,6 @@ en:
manual: Existing folder manually managed
remove_project:
deletion_failure_flash: Failed to remove the project from the storage. %{error}
dialog:
automatically_managed_appendix: Also, in this case this storage has an automatically managed project folder, this and its files will be deleted forever.
confirmation_text: Please, confirm you understand and want to remove this file storage from this project
heading_text: Remove project from %{storage}
text: This action is irreversible and will remove all links from work packages of this project to files and folders of that storage.
title: Remove project from storage
label: Remove project
services:
attributes:
@@ -227,12 +221,13 @@ en:
confirm_replace_oauth_application: This action will reset the current OAuth credentials. After confirming you will have to reenter the credentials at the storage provider and all remote users will have to authorize against OpenProject again. Are you sure you want to proceed?
confirm_replace_oauth_client: This action will reset the current OAuth credentials. After confirming you will have to enter new credentials from the storage provider and all users will have to authorize against %{provider_type} again. Are you sure you want to proceed?
delete_warning:
project_storage_delete_result_1: Remove all links from work packages of this project to files and folders of that storage.
project_storage_delete_result_2: In case this storage has an automatically managed project folder, this and its files will be deleted forever.
storage: 'Are you sure you want to delete %{file_storage}? This will:'
storage_delete_result_1: Remove all storage setups for all projects using this storage.
storage_delete_result_2: Remove all links from work packages of all projects to files and folders of that storage.
storage_delete_result_3: In case this storage has automatically managed project folders, those and their contained files will be deleted forever.
project_storage: Are you sure you want to remove %{file_storage} from this project?
project_storage_delete_result_1: All links to corresponding files and folders will be removed
project_storage_delete_result_2: The automatically-managed project folder and all files in it will be deleted
storage: Are you sure you want to delete %{file_storage} as an external file storage?
storage_delete_result_1: The storage will be removed from all projects currently using it
storage_delete_result_2: All links to corresponding files and folders will be removed
storage_delete_result_3: The automatically-managed project folder and all files in it will be deleted
dependencies:
nextcloud:
group_folders_app: Group Folders
@@ -497,7 +492,7 @@ en:
title: We are setting up your permissions on the project folder.
page_titles:
file_storages:
delete: Delete file storage
delete: Delete file storage?
subtitle: Add an external file storage in order to upload, link and manage files in work packages.
managed_project_folders:
subtitle: |-
@@ -512,6 +507,8 @@ en:
project_storage_members:
subtitle: Check the connection status for the storage %{storage_name_link} of all project members.
title: Members connection status
project_storages:
delete: Remove file storage from project?
permission_header_explanation: File permissions on external storages are applied only to folders and files within automatically managed project folders. Note that not all file permissions are supported by all storage providers. Please check the documentation on <a target='_blank' href='https://www.openproject.org/docs/system-admin-guide/users-permissions/roles-permissions/#file-storages-permissions'>file storage permissions</a> for more information.
provider_types:
label: Provider type
@@ -1,76 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
#
require "spec_helper"
require_module_spec_helper
RSpec.describe Storages::ProjectStorages::DestroyConfirmationDialogComponent,
type: :component do
describe "#heading" do
it "contains the storage name" do
storage = build_stubbed(:one_drive_storage)
project_storage = build_stubbed(:project_storage, storage:)
component = described_class.new(storage:, project_storage:)
expect(component.heading).to include(storage.name)
end
end
describe "#text" do
let(:storage) { build_stubbed(:storage, :as_generic) }
context "for a project with automatically managed project folder" do
it "includes an additional hint for data loss" do
project_storage = build_stubbed(:project_storage, storage:, project_folder_mode: "automatic")
component = described_class.new(storage:, project_storage:)
expect(component.text).to include("irreversible")
expect(component.text).to include("files will be deleted forever")
end
end
context "for a project with manual managed folder" do
it "includes only the basic hint" do
project_storage = build_stubbed(:project_storage, storage:, project_folder_mode: "manual")
component = described_class.new(storage:, project_storage:)
expect(component.text).to include("irreversible")
expect(component.text).not_to include("files will be deleted forever")
end
end
context "for a project with unmanaged folder" do
it "includes only the basic hint" do
project_storage = build_stubbed(:project_storage, storage:, project_folder_mode: "inactive")
component = described_class.new(storage:, project_storage:)
expect(component.text).to include("irreversible")
expect(component.text).not_to include("files will be deleted forever")
end
end
end
end
@@ -75,11 +75,11 @@ RSpec.describe "Delete ProjectStorage with FileLinks", :js, :webmock do
# Press Delete icon to remove the storage from the project
page.find(".icon.icon-delete").click
# Danger zone confirmation flow
# Danger dialog confirmation flow
within_test_selector("op-project-storages--delete-dialog") do
expect(page).to have_text("Delete file storage")
expect(page).to have_unchecked_field("I understand that this deletion cannot be reversed")
expect(page).to have_button("Delete permanently", disabled: true)
expect(page).to have_unchecked_field("I understand that this removal cannot be reversed")
expect(page).to have_button("Remove permanently", disabled: true)
# Cancel Confirmation
page.click_button("Cancel")
@@ -93,8 +93,8 @@ RSpec.describe "Delete ProjectStorage with FileLinks", :js, :webmock do
within_test_selector("op-project-storages--delete-dialog") do
# Approve Confirmation
page.check "I understand that this deletion cannot be reversed"
page.click_button("Delete permanently")
page.check "I understand that this removal cannot be reversed"
page.click_button("Remove permanently")
end
# List of ProjectStorages empty again
@@ -178,8 +178,8 @@ RSpec.describe("Activation of storages in projects",
within_test_selector("op-project-storages--delete-dialog") do
expect(page).to have_text("Delete file storage")
expect(page).to have_unchecked_field("I understand that this deletion cannot be reversed")
expect(page).to have_button("Delete permanently", disabled: true)
expect(page).to have_unchecked_field("I understand that this removal cannot be reversed")
expect(page).to have_button("Remove permanently", disabled: true)
# Cancel Confirmation
page.click_button("Cancel")
@@ -192,8 +192,8 @@ RSpec.describe("Activation of storages in projects",
within_test_selector("op-project-storages--delete-dialog") do
# Approve Confirmation
page.check "I understand that this deletion cannot be reversed"
page.click_button("Delete permanently")
page.check "I understand that this removal cannot be reversed"
page.click_button("Remove permanently")
end
# List of ProjectStorages empty again
@@ -367,8 +367,7 @@ RSpec.describe "Admin lists project mappings for a storage",
project_storages_index_page.click_menu_item_of("Remove project", project)
page.within("dialog") do
expect(page).to have_text("Remove project from #{storage.name}")
expect(page).to have_text("this storage has an automatically managed project folder")
expect(page).to have_text("Are you sure you want to remove #{storage.name} from this project?")
click_on "Cancel"
end
@@ -398,9 +397,9 @@ RSpec.describe "Admin lists project mappings for a storage",
page.within("dialog") do
expect(page).to have_button("Remove", disabled: true)
Retryable.repeat_until_success do
check "Please, confirm you understand and want to remove this file storage from this project", allow_label_click: true
check "I understand that this removal cannot be reversed", allow_label_click: true
expect(page).to have_button("Remove", disabled: false) # ensure button is clickable
click_on "Remove"
click_on "Remove permanently"
end
end
@@ -19,7 +19,7 @@ fr:
today: 'Aujourd''hui'
drag_here_to_remove: 'Faites glisser ici pour supprimer le responsable et les dates de début et de fin.'
cannot_drag_here: 'Cannot move the work package due to permissions or editing restrictions.'
cannot_drag_to_non_working_day: 'Ce lot de travaux ne peut pas démarrer/terminer sur un jour non ouvré.'
cannot_drag_to_non_working_day: 'Ce lot de travail ne peut pas démarrer/terminer sur un jour non ouvré.'
quick_add:
empty_state: 'Utilisez le champ de recherche pour trouver des lots de travaux et faites-les glisser vers le planificateur pour l''assigner à quelqu''un et définir des dates de début et de fin.'
search_placeholder: 'Rechercher...'
@@ -178,7 +178,7 @@ ro:
label_expiration_hint: "%{date} sau la deconectare"
label_actions: "Acțiuni"
label_confirmed: "Confirmat"
button_continue: "Continuă"
button_continue: "Continuaţi"
button_make_default: "Marcați ca implicit"
label_unverified_phone: "Telefonul mobil nu a fost încă verificat"
notice_phone_number_format: "Te rog să introduci numărul în următorul format: +XX XXXXXXXX."
@@ -178,7 +178,7 @@ ru:
label_expiration_hint: "%{date} или при выходе из системы"
label_actions: "Действия"
label_confirmed: "Подтвержден"
button_continue: "Продолжить"
button_continue: "Далее"
button_make_default: "Задать по умолчанию"
label_unverified_phone: "Сотовый телефон еще не подтвержден"
notice_phone_number_format: "Введите номер в следующем формате: +XX XXXXXXXX."
@@ -119,7 +119,7 @@ uk:
failed_to_delete: "Не вдалося видалити пристрій 2FA."
is_default_cannot_delete: "Пристрій позначено як типовий і його не можна видалити через активну політику безпеки. Перед видаленням позначте інший пристрій як стандартний."
not_existing: "Для вашого облікового запису не зареєстровано жодного пристрою 2FA."
2fa_from_input: Введіть код, отриманий на пристрій %{device_name}, щоб підтвердити свою особу.
2fa_from_input: Введіть код, що надійшов на пристрій %{device_name}, щоб підтвердити свою особу.
2fa_from_webauthn: Укажіть пристрій WebAuthn <strong>%{device_name}</strong>. Якщо це USB-пристрій, переконайтеся, що його підключено, і торкніться його. Потім натисніть кнопку входу.
webauthn:
title: "WebAuthn"
@@ -13,4 +13,4 @@ zh-CN:
xls_with_relations: "带关系的 XLS"
xls_export:
child_of: 此项的子项
parent_of: 此项的父
parent_of: 此项的父
+38 -1
View File
@@ -82,7 +82,28 @@ RSpec.describe Users::UpdateContract do
end
end
context "when global user" do
context "when user is an admin" do
let(:current_user) { create(:admin) }
describe "can update the email" do
before do
user.mail = "a.new@email.address"
end
it_behaves_like "contract is valid"
end
describe "can update the password" do
before do
user.password = "newpassword"
user.password_confirmation = "newpassword"
end
it_behaves_like "contract is valid"
end
end
context "when user with global manage_user permission" do
let(:current_user) { create(:user, global_permissions: :manage_user) }
describe "can lock the user" do
@@ -98,6 +119,14 @@ RSpec.describe Users::UpdateContract do
it_behaves_like "contract is invalid"
end
describe "cannot update the email" do
before do
user.mail = "a.new@email.address"
end
it_behaves_like "contract is invalid", mail: :error_readonly
end
end
context "when updated user is current user" do
@@ -113,6 +142,14 @@ RSpec.describe Users::UpdateContract do
it_behaves_like "contract is invalid", status: :error_readonly
end
describe "can update the email" do
before do
user.mail = "a.new@email.address"
end
it_behaves_like "contract is valid"
end
end
end
end
+1 -1
View File
@@ -771,7 +771,7 @@ RSpec.describe UsersController do
value: "another_email@example.com",
edited_user: :some_admin,
current_user: :admin
include_examples "it can update field",
include_examples "it cannot update field",
field: :mail,
value: "another_email@example.com",
edited_user: :some_user,
@@ -0,0 +1,77 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require "spec_helper"
RSpec.describe Redmine::Acts::Customizable, "validation skipping" do
let(:klass) do
Class.new(ApplicationRecord) do
include Redmine::Acts::Customizable
# Just setting it to some existing table to avoid problems
self.table_name = "users"
def self.name = "CustomizableModelForSpec"
end
end
describe ".acts_as_customizable" do
# rubocop:disable RSpec/MessageSpies
context "with :validate_on option" do
it "calls the validation with on: option" do
expect(klass).to receive(:validate).with(:validate_custom_values, on: :update)
klass.acts_as_customizable validate_on: :update
end
end
context "with :validate_except_on option" do
it "calls the validation with except: option" do
expect(klass).to receive(:validate).with(:validate_custom_values, except_on: :create)
klass.acts_as_customizable validate_except_on: :create
end
end
context "with :validate_if option" do
it "calls the validation with if: option" do
condition = ->(model) { model.some_condition? }
expect(klass).to receive(:validate).with(:validate_custom_values, if: condition)
klass.acts_as_customizable validate_if: condition
end
end
context "with :validate_unless option" do
it "calls the validation with unless: option" do
condition = ->(model) { model.some_condition? }
expect(klass).to receive(:validate).with(:validate_custom_values, unless: condition)
klass.acts_as_customizable validate_unless: condition
end
end
# rubocop:enable RSpec/MessageSpies
end
end
+40 -32
View File
@@ -23,7 +23,7 @@
#
# 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.
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
@@ -64,10 +64,11 @@ RSpec.describe "API V3 Authentication" do
context "with an invalid access token" do
let(:oauth_access_token) { "1337" }
let(:expected_www_auth_header) { 'Bearer realm="OpenProject API", scope="api_v3", error="invalid_token"' }
it "returns unauthorized" do
expect(last_response).to have_http_status :unauthorized
expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API", error="invalid_token"')
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
end
end
@@ -75,10 +76,11 @@ RSpec.describe "API V3 Authentication" do
context "with a revoked access token" do
let(:token) { create(:oauth_access_token, resource_owner: user, revoked_at: DateTime.now) }
let(:oauth_access_token) { token.plaintext_token }
let(:expected_www_auth_header) { 'Bearer realm="OpenProject API", scope="api_v3", error="invalid_token"' }
it "returns unauthorized" do
expect(last_response).to have_http_status :unauthorized
expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API", error="invalid_token"')
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
end
end
@@ -86,10 +88,11 @@ RSpec.describe "API V3 Authentication" do
context "when the token's application is disabled" do
let(:token) { create(:oauth_access_token, resource_owner: user, application: create(:oauth_application, enabled: false)) }
let(:oauth_access_token) { token.plaintext_token }
let(:expected_www_auth_header) { 'Bearer realm="OpenProject API", scope="api_v3", error="invalid_token"' }
it "returns unauthorized" do
expect(last_response).to have_http_status :unauthorized
expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API", error="invalid_token"')
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
end
end
@@ -97,6 +100,7 @@ RSpec.describe "API V3 Authentication" do
context "with an expired access token" do
let(:token) { create(:oauth_access_token, resource_owner: user) }
let(:oauth_access_token) { token.plaintext_token }
let(:expected_www_auth_header) { 'Bearer realm="OpenProject API", scope="api_v3", error="invalid_token"' }
around do |ex|
Timecop.freeze(Time.current + (token.expires_in + 5).seconds) do
@@ -106,7 +110,7 @@ RSpec.describe "API V3 Authentication" do
it "returns unauthorized" do
expect(last_response).to have_http_status :unauthorized
expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API", error="invalid_token"')
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
end
end
@@ -114,10 +118,11 @@ RSpec.describe "API V3 Authentication" do
context "with wrong scope" do
let(:token) { create(:oauth_access_token, resource_owner: user, scopes: "unknown_scope") }
let(:oauth_access_token) { token.plaintext_token }
let(:expected_www_auth_header) { 'Bearer realm="OpenProject API", scope="api_v3", error="insufficient_scope"' }
it "returns forbidden" do
expect(last_response).to have_http_status :forbidden
expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API", error="insufficient_scope"')
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
end
end
@@ -126,6 +131,7 @@ RSpec.describe "API V3 Authentication" do
let(:token) { create(:oauth_access_token, resource_owner: user, application:) }
let(:application) { create(:oauth_application) }
let(:oauth_access_token) { token.plaintext_token }
let(:expected_www_auth_header) { 'Bearer realm="OpenProject API", scope="api_v3", error="invalid_token"' }
around do |ex|
user.destroy
@@ -134,7 +140,7 @@ RSpec.describe "API V3 Authentication" do
it "returns unauthorized" do
expect(last_response).to have_http_status :unauthorized
expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API", error="invalid_token"')
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
end
@@ -143,7 +149,7 @@ RSpec.describe "API V3 Authentication" do
it "returns unauthorized" do
expect(last_response).to have_http_status :unauthorized
expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API", error="invalid_token"')
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
end
end
@@ -153,10 +159,11 @@ RSpec.describe "API V3 Authentication" do
let(:token) { create(:oauth_access_token, resource_owner: user) }
let(:oauth_access_token) { token.plaintext_token }
let(:user) { create(:user, :locked) }
let(:expected_www_auth_header) { 'Bearer realm="OpenProject API", scope="api_v3", error="invalid_token"' }
it "returns unauthorized" do
expect(last_response).to have_http_status :unauthorized
expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API", error="invalid_token"')
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
end
end
@@ -189,10 +196,11 @@ RSpec.describe "API V3 Authentication" do
context "and the client credentials user is locked" do
let(:user) { create(:user, :locked) }
let(:expected_message) { "You did not provide the correct credentials." }
let(:expected_www_auth_header) { 'Bearer realm="OpenProject API", scope="api_v3", error="invalid_token"' }
it "returns unauthorized" do
expect(last_response).to have_http_status :unauthorized
expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API", error="invalid_token"')
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
end
end
@@ -491,6 +499,11 @@ RSpec.describe "API V3 Authentication" do
let(:token_issuer) { "https://keycloak.local/realms/master" }
let(:token_scope) { "email profile api_v3" }
let(:expected_message) { "You did not provide the correct credentials." }
let(:expected_www_auth_header) do
"Bearer realm=\"OpenProject API\", scope=\"api_v3\", error=\"#{expected_error}\", " \
"error_description=\"#{expected_error_description}\""
end
let(:expected_error) { "invalid_token" }
let(:keys_request_stub) do
stub_request(:get, "https://keycloak.local/realms/master/protocol/openid-connect/certs")
.to_return(status: 200, body: JWT::JWK::Set.new(jwk_response).export.to_json, headers: {})
@@ -514,66 +527,64 @@ RSpec.describe "API V3 Authentication" do
context "when token is issued by provider not configured in OP" do
let(:token_issuer) { "https://eve.example.com" }
let(:expected_error_description) { "The access token issuer is unknown" }
it "fails with HTTP 401 Unauthorized" do
get resource
expect(last_response).to have_http_status :unauthorized
expect(last_response.header["WWW-Authenticate"])
.to eq(%{Bearer realm="OpenProject API", error="invalid_token", error_description="The access token issuer is unknown"})
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
end
end
context "when token signature algorithm is not supported" do
let(:token) { JWT.encode(payload, "secret", "HS256", { kid: "97AmyvoS8BFFRfm585GPgA16G1H2V22EdxxuAYUuoKk" }) }
let(:expected_error_description) { "Token signature algorithm HS256 is not supported" }
it "fails with HTTP 401 Unauthorized" do
get resource
expect(last_response).to have_http_status :unauthorized
error = "Token signature algorithm HS256 is not supported"
expect(last_response.header["WWW-Authenticate"])
.to eq(%{Bearer realm="OpenProject API", error="invalid_token", error_description="#{error}"})
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
end
end
context "when aud does not contain client_id" do
let(:token_aud) { ["Lisa", "Bart"] }
let(:expected_error_description) { 'Invalid audience. Expected https://openproject.local, received [\"Lisa\", \"Bart\"]' }
it "fails with HTTP 401 Unauthorized" do
get resource
expect(last_response).to have_http_status :unauthorized
error = 'Invalid audience. Expected https://openproject.local, received ["Lisa", "Bart"]'
expect(last_response.header["WWW-Authenticate"])
.to eq(%{Bearer realm="OpenProject API", error="invalid_token", error_description="#{error}"})
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
end
end
context "when the scope does not permit access to APIv3" do
let(:token_scope) { "profile email" }
let(:expected_error) { "insufficient_scope" }
let(:expected_error_description) { "Requires scope api_v3 to access this resource." }
it "fails with HTTP 403 Forbidden" do
get resource
expect(last_response).to have_http_status :forbidden
error = "Requires scope api_v3 to access this resource."
expect(last_response.header["WWW-Authenticate"])
.to eq(%{Bearer realm="OpenProject API", error="insufficient_scope", error_description="#{error}"})
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
end
end
context "when access token has expired already" do
let(:token_exp) { 5.minutes.ago }
let(:expected_error_description) { "Signature has expired" }
it "fails with HTTP 401 Unauthorized" do
get resource
expect(last_response).to have_http_status :unauthorized
expect(last_response.header["WWW-Authenticate"])
.to eq(%{Bearer realm="OpenProject API", error="invalid_token", error_description="Signature has expired"})
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
end
@@ -590,40 +601,37 @@ RSpec.describe "API V3 Authentication" do
context "when kid is absent in keycloak keys response" do
let(:jwk_response) { JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid: "your-kid", use: "sig", alg: "RS256") }
let(:expected_error_description) { "The signature key ID is unknown" }
it "fails with HTTP 401 Unauthorized" do
get resource
expect(last_response).to have_http_status :unauthorized
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
error = "The signature key ID is unknown"
expect(last_response.header["WWW-Authenticate"])
.to eq(%{Bearer realm="OpenProject API", error="invalid_token", error_description="#{error}"})
end
end
context "when user identified by token is not known" do
let(:user) { create(:user, authentication_provider: create(:oidc_provider)) }
let(:expected_error_description) { "The user identified by the token is not known" }
it "fails with HTTP 401 Unauthorized" do
get resource
expect(last_response).to have_http_status :unauthorized
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
error = "The user identified by the token is not known"
expect(last_response.header["WWW-Authenticate"])
.to eq(%{Bearer realm="OpenProject API", error="invalid_token", error_description="#{error}"})
end
end
context "when user identified by token is locked" do
let(:user) { create(:user, :locked, authentication_provider: create(:oidc_provider), external_id: token_sub) }
let(:expected_error_description) { "The user account is locked" }
it "fails with HTTP 401 Unauthorized" do
get resource
expect(last_response).to have_http_status :unauthorized
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(JSON.parse(last_response.body)).to eq(error_response_body)
error = "The user account is locked"
expect(last_response.header["WWW-Authenticate"])
.to eq(%{Bearer realm="OpenProject API", error="invalid_token", error_description="#{error}"})
end
end
end
@@ -73,8 +73,8 @@ RSpec.describe API::V3::Users::UpdateFormAPI, content_type: :json do
expect(response.body).to be_json_eql("Form".to_json).at_path("_type")
expect(body)
.to be_json_eql(user.mail.to_json)
.at_path("_embedded/payload/email")
.to be_json_eql(user.login.to_json)
.at_path("_embedded/payload/login")
expect(body)
.to be_json_eql(user.firstname.to_json)
@@ -127,8 +127,8 @@ RSpec.describe API::V3::Users::UpdateFormAPI, content_type: :json do
expect(response.body).to be_json_eql("Form".to_json).at_path("_type")
expect(body)
.to be_json_eql(user.mail.to_json)
.at_path("_embedded/payload/email")
.to be_json_eql(user.login.to_json)
.at_path("_embedded/payload/login")
expect(body)
.not_to have_json_path("_embedded/payload/firstName")
@@ -174,7 +174,7 @@ RSpec.describe API::V3::Users::UpdateFormAPI, content_type: :json do
context "when no custom field value is provided" do
let(:payload) do
{
email: "updated@example.org"
login: "new.login"
}
end
@@ -182,8 +182,8 @@ RSpec.describe API::V3::Users::UpdateFormAPI, content_type: :json do
expect(response).to have_http_status(:ok)
expect(body).to have_json_size(0).at_path("_embedded/validationErrors")
expect(body)
.to be_json_eql("updated@example.org".to_json)
.at_path("_embedded/payload/email")
.to be_json_eql("new.login".to_json)
.at_path("_embedded/payload/login")
expect(body)
.to be_json_eql(api_v3_paths.user(user.id).to_json)
.at_path("_links/commit/href")
@@ -193,7 +193,7 @@ RSpec.describe API::V3::Users::UpdateFormAPI, content_type: :json do
context "when the custom field is provided but empty" do
let(:payload) do
{
email: "updated@example.org",
login: "new.login",
required_custom_field.attribute_name(:camel_case) => {
raw: ""
}
@@ -213,7 +213,7 @@ RSpec.describe API::V3::Users::UpdateFormAPI, content_type: :json do
context "when the custom field value is provided and valid" do
let(:payload) do
{
email: "updated@example.org",
login: "new.login",
required_custom_field.attribute_name(:camel_case) => {
raw: "Engineering"
}
@@ -240,7 +240,7 @@ RSpec.describe API::V3::Users::UpdateFormAPI, content_type: :json do
let(:payload) do
{
email: "updated@example.org",
login: "new.login",
visible_custom_field.attribute_name(:camel_case) => {
raw: "CF text"
}
@@ -269,7 +269,7 @@ RSpec.describe API::V3::Users::UpdateFormAPI, content_type: :json do
let(:current_user) { create(:admin) }
let(:payload) do
{
email: "updated@example.org",
login: "new.login",
admin_only_custom_field.attribute_name(:camel_case) => {
raw: "CF text"
}
@@ -291,7 +291,7 @@ RSpec.describe API::V3::Users::UpdateFormAPI, content_type: :json do
context "with non-admin permissions" do
let(:payload) do
{
email: "updated@example.org",
login: "new.login",
admin_only_custom_field.attribute_name(:camel_case) => {
raw: "CF text"
}
@@ -312,7 +312,7 @@ RSpec.describe API::V3::Users::UpdateFormAPI, content_type: :json do
let(:is_required) { true }
let(:payload) do
{
email: "updated@example.org"
login: "new.login"
}
end
@@ -69,14 +69,14 @@ RSpec.describe API::V3::Users::UsersAPI do
end
describe "attribute change" do
let(:parameters) { { email: "foo@example.org", language: "de" } }
let(:parameters) { { login: "new.login", language: "de" } }
it_behaves_like "successful update", mail: "foo@example.org", language: "de"
it_behaves_like "successful update", login: "new.login", language: "de"
end
describe "attribute collision" do
let(:parameters) { { email: "foo@example.org" } }
let(:collision) { create(:user, mail: "foo@example.org") }
let(:parameters) { { login: "new.login" } }
let(:collision) { create(:user, login: "new.login") }
before do
collision
@@ -88,7 +88,7 @@ RSpec.describe API::V3::Users::UsersAPI do
expect(last_response).to have_http_status(:unprocessable_entity)
expect(last_response.body)
.to be_json_eql("email".to_json)
.to be_json_eql("login".to_json)
.at_path("_embedded/details/attribute")
expect(last_response.body)
@@ -127,11 +127,11 @@ RSpec.describe API::V3::Users::UsersAPI do
context "when no custom field value is provided" do
let(:parameters) do
{
email: "updated@example.org"
login: "new.login"
}
end
it_behaves_like "successful update", mail: "updated@example.org"
it_behaves_like "successful update", login: "new.login"
it "keeps the custom field value empty" do
send_request
@@ -143,7 +143,7 @@ RSpec.describe API::V3::Users::UsersAPI do
context "when the custom field is provided but empty" do
let(:parameters) do
{
email: "updated@example.org",
login: "new.login",
required_custom_field.attribute_name(:camel_case) => {
raw: ""
}
@@ -165,14 +165,14 @@ RSpec.describe API::V3::Users::UsersAPI do
context "when the custom field value is provided and valid" do
let(:parameters) do
{
email: "updated@example.org",
login: "new.login",
required_custom_field.attribute_name(:camel_case) => {
raw: "Engineering"
}
}
end
it_behaves_like "successful update", mail: "updated@example.org"
it_behaves_like "successful update", login: "new.login"
it "updates the user with the custom field value" do
send_request
@@ -189,14 +189,14 @@ RSpec.describe API::V3::Users::UsersAPI do
let(:parameters) do
{
email: "updated@example.org",
login: "new.login",
custom_field.attribute_name(:camel_case) => {
raw: "CF text"
}
}
end
it_behaves_like "successful update", mail: "updated@example.org"
it_behaves_like "successful update", login: "new.login"
it "sets the cf value" do
send_request
@@ -215,14 +215,14 @@ RSpec.describe API::V3::Users::UsersAPI do
let(:current_user) { create(:admin) }
let(:parameters) do
{
email: "updated@example.org",
login: "new.login",
admin_only_custom_field.attribute_name(:camel_case) => {
raw: "CF text"
}
}
end
it_behaves_like "successful update", mail: "updated@example.org"
it_behaves_like "successful update", login: "new.login"
it "sets the cf value" do
send_request
@@ -235,14 +235,14 @@ RSpec.describe API::V3::Users::UsersAPI do
let(:current_user) { create(:user, global_permissions: %i[manage_user view_all_principals]) }
let(:parameters) do
{
email: "updated@example.org",
login: "new.login",
admin_only_custom_field.attribute_name(:camel_case) => {
raw: "CF text"
}
}
end
it_behaves_like "successful update", mail: "updated@example.org"
it_behaves_like "successful update", login: "new.login"
it "does not set the cf value" do
send_request
@@ -254,11 +254,11 @@ RSpec.describe API::V3::Users::UsersAPI do
let(:is_required) { true }
let(:parameters) do
{
email: "updated@example.org"
login: "new.login"
}
end
it_behaves_like "successful update", mail: "updated@example.org"
it_behaves_like "successful update", login: "new.login"
it "does not set the cf value" do
send_request
@@ -290,8 +290,21 @@ RSpec.describe API::V3::Users::UsersAPI do
end
end
describe "email update" do
let(:email) { "this.is.a.new@email.address" }
let(:parameters) { { email: email } }
it "updates the users email correctly" do
send_request
expect(last_response).to have_http_status(:ok)
updated_user = User.find(user.id)
expect(updated_user.mail).to eq(email)
end
end
describe "unknown user" do
let(:parameters) { { email: "foo@example.org" } }
let(:parameters) { { login: "new.login" } }
let(:path) { api_v3_paths.user(666) }
it "responds with 404" do
@@ -324,6 +337,24 @@ RSpec.describe API::V3::Users::UsersAPI do
.at_path("errorIdentifier")
end
end
describe "email update" do
let(:email) { "this.is.a.new@email.address" }
let(:parameters) { { email: email } }
it "rejects the users email update" do
send_request
expect(last_response).to have_http_status(:unprocessable_entity)
expect(last_response.body)
.to be_json_eql("email".to_json)
.at_path("_embedded/details/attribute")
expect(last_response.body)
.to be_json_eql("urn:openproject-org:api:v3:errors:PropertyIsReadOnly".to_json)
.at_path("errorIdentifier")
end
end
end
describe "unauthorized user" do
+12 -3
View File
@@ -23,7 +23,7 @@
#
# 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.
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
@@ -125,16 +125,25 @@ RSpec.describe "SCIM API Authentication" do
context "when scim_v2 scope is missing in token" do
let(:token_scope) { "api_v3" }
let(:expected_www_auth_header) do
'Bearer realm="OpenProject API", scope="scim_v2", error="insufficient_scope", ' \
'error_description="Requires scope scim_v2 to access this resource."'
end
it do
get "/scim_v2/ServiceProviderConfig", {}, headers
expect(last_response.body).to eq("insufficient_scope")
expect(last_response.headers["WWW-Authenticate"]).to eq("Bearer realm=\"OpenProject API\", error=\"insufficient_scope\", error_description=\"Requires scope scim_v2 to access this resource.\"")
expect(last_response.headers["WWW-Authenticate"]).to eq(expected_www_auth_header)
expect(last_response).to have_http_status(401)
end
end
context "when token_sub does not match a service_account" do
let(:expected_www_auth_header) do
'Bearer realm="OpenProject API", scope="scim_v2", error="invalid_token", ' \
'error_description="The user identified by the token is not known"'
end
before { service_account.user_auth_provider_links.delete_all }
it do
@@ -142,7 +151,7 @@ RSpec.describe "SCIM API Authentication" do
expect(last_response).to have_http_status(401)
expect(last_response.body).to eq("invalid_token")
expect(last_response.headers["WWW-Authenticate"]).to eq("Bearer realm=\"OpenProject API\", error=\"invalid_token\", error_description=\"The user identified by the token is not known\"")
expect(last_response.headers["WWW-Authenticate"]).to eq(expected_www_auth_header)
end
end
end
@@ -205,11 +205,11 @@ RSpec.describe Projects::CreateArtifactWorkPackageService do
date = Date.current.iso8601
expect(Storages::UploadFileService)
.to have_received(:call)
.with(container: artifact_work_package,
project_storage:,
file_path: "Project mandate",
filename: /#{project.identifier}_Project_mandate_#{artifact_work_package.status.name}_#{date}_\d+-\d+.pdf/,
file_data: instance_of(StringIO))
.with(container: artifact_work_package,
project_storage:,
file_path: "Project mandate",
filename: /#{project.identifier}_Project_mandate_#{artifact_work_package.status.name}_#{date}_\d+-\d+.pdf/,
file_data: instance_of(StringIO))
end
context "with another default language", with_settings: { default_language: :de } do