Merge branch 'dev' into feature/63912-support-multiple-authentication-provider-user-links

This commit is contained in:
Pavel Balashou
2025-06-11 10:18:34 +02:00
committed by GitHub
548 changed files with 13622 additions and 1836 deletions
+1 -1
View File
@@ -142,7 +142,7 @@ Naming/ClassAndModuleCamelCase:
Naming/FileName:
Enabled: false
Naming/PredicateName:
Naming/PredicatePrefix:
ForbiddenPrefixes:
- is_
+4 -4
View File
@@ -36,7 +36,7 @@ ruby File.read(File.expand_path(".ruby-version", __dir__)).strip
gem "actionpack-xml_parser", "~> 2.0.0"
gem "activemodel-serializers-xml", "~> 1.0.1"
gem "activerecord-import", "~> 2.1.0"
gem "activerecord-import", "~> 2.2.0"
gem "activerecord-session_store", "~> 2.2.0"
gem "ox"
gem "rails", "~> 8.0.1"
@@ -62,7 +62,7 @@ gem "friendly_id", "~> 5.5.0"
gem "acts_as_list", "~> 1.2.0"
gem "acts_as_tree", "~> 2.9.0"
gem "awesome_nested_set", "~> 3.8.0"
gem "closure_tree", "~> 7.4.0"
gem "closure_tree", "~> 8.0.0"
gem "rubytree", "~> 2.1.0"
# Only used in down migrations now.
# Is to be removed once the referencing migrations have been squashed.
@@ -225,7 +225,7 @@ gem "appsignal", "~> 3.10.0", require: false
# Yabeda integration
gem "yabeda-activerecord"
gem "yabeda-prometheus-mmap"
gem "yabeda-prometheus-mmap", require: false
gem "yabeda-puma-plugin"
gem "yabeda-rails"
@@ -400,7 +400,7 @@ platforms :mri, :mingw, :x64_mingw do
# Have application level locks on the database to have a mutex shared between workers/hosts.
# We e.g. employ this to safeguard the creation of journals.
gem "with_advisory_lock", "~> 5.1.0"
gem "with_advisory_lock", "~> 5.3.0"
end
# Load Gemfile.modules explicitly to allow dependabot to work
+70 -70
View File
@@ -208,7 +208,7 @@ PATH
remote: modules/two_factor_authentication
specs:
openproject-two_factor_authentication (1.0.0)
aws-sdk-sns (~> 1.99.0)
aws-sdk-sns (~> 1.100.0)
messagebird-rest (~> 1.4.2)
rotp (~> 6.1)
webauthn (~> 3.0)
@@ -289,7 +289,7 @@ GEM
activemodel (= 8.0.2)
activesupport (= 8.0.2)
timeout (>= 0.4.0)
activerecord-import (2.1.0)
activerecord-import (2.2.0)
activerecord (>= 4.2)
activerecord-nulldb-adapter (1.1.1)
activerecord (>= 6.0, < 8.1)
@@ -342,26 +342,26 @@ GEM
activerecord (>= 4.0)
awesome_nested_set (3.8.0)
activerecord (>= 4.0.0, < 8.1)
aws-eventstream (1.3.2)
aws-partitions (1.1105.0)
aws-sdk-core (3.224.0)
aws-eventstream (1.4.0)
aws-partitions (1.1115.0)
aws-sdk-core (3.225.2)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.101.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (1.104.0)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.186.1)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-s3 (1.189.1)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sdk-sns (1.99.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-sns (1.100.0)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
axe-core-api (4.10.3)
dumb_delegator
@@ -378,7 +378,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1)
base64 (0.2.0)
bcrypt (3.1.20)
benchmark (0.4.0)
benchmark (0.4.1)
better_html (2.1.1)
actionview (>= 6.0)
activesupport (>= 6.0)
@@ -386,7 +386,7 @@ GEM
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bigdecimal (3.2.0)
bigdecimal (3.2.1)
bindata (2.5.1)
bootsnap (1.18.6)
msgpack (~> 1.2)
@@ -416,13 +416,13 @@ GEM
carrierwave (>= 1.0.0)
fog-aws
cbor (0.5.9.8)
cgi (0.4.2)
cgi (0.5.0)
childprocess (5.1.0)
logger (~> 1.5)
climate_control (1.2.0)
closure_tree (7.4.0)
activerecord (>= 4.2.10)
with_advisory_lock (>= 4.0.0)
closure_tree (8.0.0)
activerecord (>= 7.1.0)
with_advisory_lock (>= 5.0.0, < 6.0.0)
coderay (1.1.3)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
@@ -450,7 +450,7 @@ GEM
crass (1.0.6)
css_parser (1.21.1)
addressable
csv (3.3.4)
csv (3.3.5)
cuprite (0.17)
capybara (~> 3.0)
ferrum (~> 0.17.0)
@@ -626,7 +626,7 @@ GEM
fugit (>= 1.1)
railties (>= 6.0.0)
thor (>= 0.14.1)
google-apis-core (0.17.0)
google-apis-core (0.18.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 1.9)
httpclient (>= 2.8.3, < 3.a)
@@ -636,7 +636,7 @@ GEM
retriable (>= 2.0, < 4.a)
google-apis-gmail_v1 (0.43.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-env (2.3.0)
google-cloud-env (2.3.1)
base64 (~> 0.2)
faraday (>= 1.0, < 3.a)
google-logging-utils (0.2.0)
@@ -659,7 +659,7 @@ GEM
rack
gravatar_image_tag (1.2.0)
hana (1.3.7)
hashdiff (1.1.2)
hashdiff (1.2.0)
hashery (2.1.2)
hashie (5.0.0)
highline (3.1.2)
@@ -784,7 +784,7 @@ GEM
mime-types (3.7.0)
logger
mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2025.0527)
mime-types-data (3.2025.0603)
mini_magick (5.2.0)
benchmark
logger
@@ -953,7 +953,7 @@ GEM
date
stringio
public_suffix (6.0.2)
puffing-billy (4.0.1)
puffing-billy (4.0.2)
addressable (~> 2.5)
em-http-request (~> 1.1, >= 1.1.0)
em-synchrony
@@ -967,12 +967,12 @@ GEM
puma (>= 5.0, < 7)
raabro (1.4.0)
racc (1.8.1)
rack (2.2.15)
rack (2.2.17)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-cors (2.0.2)
rack (>= 2.0.0)
rack-mini-profiler (3.3.1)
rack-mini-profiler (4.0.0)
rack (>= 1.2.0)
rack-oauth2 (2.2.1)
activesupport
@@ -1013,7 +1013,7 @@ GEM
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.2.0)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
@@ -1032,14 +1032,14 @@ GEM
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rake (13.3.0)
rake-compiler-dock (1.9.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rb_sys (0.9.115)
rb_sys (0.9.116)
rake-compiler-dock (= 1.9.1)
rbtrace (0.5.1)
rbtrace (0.5.2)
ffi (>= 1.0.6)
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
@@ -1097,7 +1097,7 @@ GEM
rspec-support (3.13.4)
rspec-wait (1.0.1)
rspec (>= 3.4)
rubocop (1.75.8)
rubocop (1.76.1)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -1105,10 +1105,10 @@ GEM
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.44.0, < 2.0)
rubocop-ast (>= 1.45.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.44.1)
rubocop-ast (1.45.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-capybara (2.22.1)
@@ -1226,7 +1226,7 @@ GEM
openssl-signature_algorithm (~> 1.0)
trailblazer-option (0.1.2)
ttfunk (1.7.0)
turbo-rails (2.0.13)
turbo-rails (2.0.16)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
turbo_power (0.7.0)
@@ -1251,7 +1251,7 @@ GEM
public_suffix
vcr (6.3.1)
base64
vernier (1.7.1)
vernier (1.8.0)
view_component (3.23.2)
activesupport (>= 5.2.0, < 8.1)
concurrent-ruby (~> 1)
@@ -1282,12 +1282,12 @@ GEM
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1)
websocket (1.2.11)
websocket-driver (0.7.7)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
will_paginate (4.0.1)
with_advisory_lock (5.1.0)
with_advisory_lock (5.3.0)
activerecord (>= 6.1)
zeitwerk (>= 2.6)
xpath (3.2.0)
@@ -1333,7 +1333,7 @@ DEPENDENCIES
actionpack-xml_parser (~> 2.0.0)
active_record_doctor (~> 1.15.0)
activemodel-serializers-xml (~> 1.0.1)
activerecord-import (~> 2.1.0)
activerecord-import (~> 2.2.0)
activerecord-nulldb-adapter (~> 1.1.1)
activerecord-session_store (~> 2.2.0)
acts_as_list (~> 1.2.0)
@@ -1358,7 +1358,7 @@ DEPENDENCIES
carrierwave (~> 1.3.4)
carrierwave_direct (~> 2.1.0)
climate_control
closure_tree (~> 7.4.0)
closure_tree (~> 8.0.0)
colored2
commonmarker (~> 2.3.0)
compare-xml (~> 0.66)
@@ -1538,7 +1538,7 @@ DEPENDENCIES
warden-basic_auth (~> 0.2.1)
webmock (~> 3.12)
will_paginate (~> 4.0.0)
with_advisory_lock (~> 5.1.0)
with_advisory_lock (~> 5.3.0)
yabeda-activerecord
yabeda-prometheus-mmap
yabeda-puma-plugin
@@ -1558,7 +1558,7 @@ CHECKSUMS
activemodel (8.0.2) sha256=0ae1fb7fa1fae0699ba041a9e97702df42ea3b13f2d39f2d0fde51fca5f0656c
activemodel-serializers-xml (1.0.3) sha256=fa1b16305e7254cc58a59c68833e3c0a593a59c8ab95d3be5aaea7cd9416c397
activerecord (8.0.2) sha256=793470b92c44e4198d0262ac60086b7822f0ea585079ad67e32a6e4c86f2d90a
activerecord-import (2.1.0) sha256=7b1bc586c4b636e125c18f533c9ac4421dd2dbac8f4865dbf576382a338c2c94
activerecord-import (2.2.0) sha256=f8ca99b196e50775723d1f1d192c379f656378dc9f5628240992a0d78807fa4b
activerecord-nulldb-adapter (1.1.1) sha256=034c91106183b954b072fba14c2786adf1a2b9e852ce04f85f823afaf03e9820
activerecord-session_store (2.2.0) sha256=65918054573683bf4f87af89e765e1fece14c9d71cfac1f11abe4687c96e2743
activestorage (8.0.2) sha256=f83d221e0f06ae38f2200e55490bd155c76d0add330f6e300e8646048d672977
@@ -1577,21 +1577,21 @@ CHECKSUMS
attr_required (1.0.2) sha256=f0ebfc56b35e874f4d0ae799066dbc1f81efefe2364ca3803dc9ea6a4de6cb99
auto_strip_attributes (2.6.0) sha256=a7e2e0cf744de2bcd947fd68014220702bcc88c81274c1cd9ce6f7316aae39b0
awesome_nested_set (3.8.0) sha256=469daff411d80291dbb80d1973133e498048a7afc2519c545f62d2cdebc60eda
aws-eventstream (1.3.2) sha256=7e2c3a55ca70d7861d5d3c98e47daa463ed539c349caba22b48305b8919572d7
aws-partitions (1.1105.0) sha256=a68010773df339ffe8074eac22de4d08e5235a1b25a895c3b2cea73ec54a6ea1
aws-sdk-core (3.224.0) sha256=c617aae3f43ba2bfe9f819c1a31c7c9dbda3e1d1a33c746c23c4dc15638817ac
aws-sdk-kms (1.101.0) sha256=44d8b5b69ce7394cc02f30f9a35bea04ea12c947b5ffe1471535eea5119368d7
aws-sdk-s3 (1.186.1) sha256=5b703b8aafc26ca4f3f927368412da5f4b8f74909bb0e4651eb8afe8aa105803
aws-sdk-sns (1.99.0) sha256=a84cf111e375783f8d92093cb790757f6856acfe15cbf46a7f1a9868ded6ec0a
aws-sigv4 (1.11.0) sha256=50a8796991a862324442036ad7a395920572b84bb6cd29b945e5e1800e8da1db
aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b
aws-partitions (1.1115.0) sha256=787dc8a8d8db2aeb1474b4ee9a71e9f8b315d8109063fea0021030a6522f9e6c
aws-sdk-core (3.225.2) sha256=3ebed026b4bb527740cdf9f2a0c1b4a542d070ee015f8dd6bfc4c265d75dd4f8
aws-sdk-kms (1.104.0) sha256=d65f13254452a9648fc3557018214e4c1809224c8538de576dd079772f0390f4
aws-sdk-s3 (1.189.1) sha256=dd46336000eb3d78ff3ba4b648dd520c83c171ac29a04f13ddb08249fd1b7de4
aws-sdk-sns (1.100.0) sha256=706bb13c5da789a5b9c6219b397980645536dd7f7301c09fbb8d3e8613f65a96
aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00
axe-core-api (4.10.3) sha256=6e10f3ed1c031804f16e8154d9d5dc658564d10850cee860e125fe665c3f0148
axe-core-rspec (4.10.3) sha256=ca21d0111e2d0fcd0f1da922c9071337336732aa6a3a8dc21bed94c9a701527e
axiom-types (0.1.1) sha256=c1ff113f3de516fa195b2db7e0a9a95fd1b08475a502ff660d04507a09980383
base64 (0.2.0) sha256=0f25e9b21a02a0cc0cea8ef92b2041035d39350946e8789c562b2d1a3da01507
bcrypt (3.1.20) sha256=8410f8c7b3ed54a3c00cd2456bf13917d695117f033218e2483b2e40b0784099
benchmark (0.4.0) sha256=0f12f8c495545e3710c3e4f0480f63f06b4c842cc94cec7f33a956f5180e874a
benchmark (0.4.1) sha256=d4ef40037bba27f03b28013e219b950b82bace296549ec15a78016552f8d2cce
better_html (2.1.1) sha256=046c3551d1488a3f2939a7cac6fabf2bde08c32e135c91fcd683380118e5af55
bigdecimal (3.2.0) sha256=f220c34e07d98b04e02eb23193bee436fab9afcd551f43dce8837a1b4aa80762
bigdecimal (3.2.1) sha256=1f68631e876c6aba8fe9b84b36983c55ad3293ff2d1ad4c6f115bde1e9d802e3
bindata (2.5.1) sha256=53186a1ec2da943d4cb413583d680644eb810aacbf8902497aac8f191fad9e58
bootsnap (1.18.6) sha256=0ae2393c1e911e38be0f24e9173e7be570c3650128251bf06240046f84a07d00
brakeman (7.0.2) sha256=b602d91bcec6c5ce4d4bc9e081e01f621c304b7a69f227d1e58784135f333786
@@ -1605,10 +1605,10 @@ CHECKSUMS
carrierwave (1.3.4) sha256=81772dabd1830edbd7f4526d2ae2c79f974f1d48900c3f03f7ecb7c657463a21
carrierwave_direct (2.1.0) sha256=b0d5c19c1d17a05940e488cff644a3b2946422d6d494b2174b48f6a90c5dddbf
cbor (0.5.9.8) sha256=9ee097fc58d9bc5e406d112cd2d4e112c7354ec16f8b6ff34e4732c1e44b4eb7
cgi (0.4.2) sha256=a3cb190d46a820ca01a3e28bd5b64e67003ff99d7884b70512448566f35347e6
cgi (0.5.0) sha256=fe99f65bb2c146e294372ebb27602adbc3b4c008e9ea7038c6bd48c1ec9759da
childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec
climate_control (1.2.0) sha256=36b21896193fa8c8536fa1cd843a07cf8ddbd03aaba43665e26c53ec1bd70aa5
closure_tree (7.4.0) sha256=e39171bbe35de3c06898c3caf2d2949bba2d379aa78fc1a03c1c63f165c1d2b5
closure_tree (8.0.0) sha256=9f33c8b2511db80d3cb418cba34da85176585bb9f1077c5d58e873e2495dbbcb
coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b
coercible (1.0.0) sha256=5081ad24352cc8435ce5472bc2faa30260c7ea7f2102cc6a9f167c4d9bffaadc
color_conversion (0.1.2) sha256=99bea5fa412e1527a11389975aa6ad445ff8528ebae202c11d08c45ea2b94c96
@@ -1629,7 +1629,7 @@ CHECKSUMS
crack (1.0.0) sha256=c83aefdb428cdc7b66c7f287e488c796f055c0839e6e545fec2c7047743c4a49
crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d
css_parser (1.21.1) sha256=6cfd3ffc0a97333b39d2b1b49c95397b05e0e3b684d68f77ec471ba4ec2ef7c7
csv (3.3.4) sha256=e96ecd5a8c3494aa5b596282249daba5c6033203c199248e6146e36d2a78d8cd
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
cuprite (0.17) sha256=b140d5dc70d08b97ad54bcf45cd95d0bd430e291e9dffe76fff851fddd57c12b
daemons (1.4.1) sha256=8fc76d76faec669feb5e455d72f35bd4c46dc6735e28c420afb822fac1fa9a1d
dalli (3.2.8) sha256=2e63595084d91fae2655514a02c5d4fc0f16c0799893794abe23bf628bebaaa5
@@ -1703,9 +1703,9 @@ CHECKSUMS
globalid (1.2.1) sha256=70bf76711871f843dbba72beb8613229a49429d1866828476f9c9d6ccc327ce9
gon (6.4.0) sha256=e3a618d659392890f1aa7db420f17c75fd7d35aeb5f8fe003697d02c4b88d2f0
good_job (3.99.1) sha256=7d3869d8a8ee8ef7048fee5d746f41c21987b7822c20038a2f773036bef0830a
google-apis-core (0.17.0) sha256=3d4408b26b3f4b517b869be3c5aba40db0e172b4481c20ff882ef47579dd08f8
google-apis-core (0.18.0) sha256=96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee
google-apis-gmail_v1 (0.43.0) sha256=037a218338b2d40b31d6f4c9206e58a6e34062fceac8a310f8dbc92b1a2b947b
google-cloud-env (2.3.0) sha256=db80b120fc163d1b83ffe8c0bc82e9ad0025ef0d51d987068c7278677ee5caf7
google-cloud-env (2.3.1) sha256=0faac01eb27be78c2591d64433663b1a114f8f7af55a4f819755426cac9178e7
google-logging-utils (0.2.0) sha256=675462b4ea5affa825a3442694ca2d75d0069455a1d0956127207498fca3df7b
googleauth (1.14.0) sha256=62e7de11791890c3d3dc70582dfd9ab5516530e4e4f56d96451fd62c76475149
grape (2.3.0) sha256=99484ae2907b06a9e109edf2911c383809bf7f7c00d65554e4d01f0388728bda
@@ -1713,7 +1713,7 @@ CHECKSUMS
gravatar_image_tag (1.2.0) sha256=eb5630fea846b711e713b934a0178fb9785f02f4eb9ced8d6faa4d537c40fdcf
grids (1.0.0)
hana (1.3.7) sha256=5425db42d651fea08859811c29d20446f16af196308162894db208cac5ce9b0d
hashdiff (1.1.2) sha256=2c30eeded6ed3dce8401d2b5b99e6963fe5f14ed85e60dd9e33c545a44b71a77
hashdiff (1.2.0) sha256=c984f13e115bfc9953332e8e83bd9d769cfde9944e2d54e07eb9df7b76e140b5
hashery (2.1.2) sha256=d239cc2310401903f6b79d458c2bbef5bf74c46f3f974ae9c1061fb74a404862
hashie (5.0.0) sha256=9d6c4e51f2a36d4616cbc8a322d619a162d8f42815a792596039fc95595603da
highline (3.1.2) sha256=67cbd34d19f6ef11a7ee1d82ffab5d36dfd5b3be861f450fc1716c7125f4bb4a
@@ -1764,7 +1764,7 @@ CHECKSUMS
meta-tags (2.22.1) sha256=e5ae1febbd320d396c7226d7edb868e5d63466c14b9c8b06622a1a74e6dce354
method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5
mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56
mime-types-data (3.2025.0527) sha256=11808d780cdb27ea5db0143dbfc261b91ed63ec5e595f424a35f9822cb497a02
mime-types-data (3.2025.0603) sha256=00a122cf046ef3867c428ed5e6d97e759027b0caa375da7fba33a9799c8a3037
mini_magick (5.2.0) sha256=2757ffbfdb1d38242d1da9ff1505360ab75d59dc02eb7ab79ff6d5acb1243f4a
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289
@@ -1860,15 +1860,15 @@ CHECKSUMS
pry-rescue (1.6.0) sha256=985bfd506d9866b587fd86790cf8445266a41b7f92c627fc5b21ec7d92aba6db
psych (5.2.6) sha256=814328aa5dcb6d604d32126a20bc1cbcf05521a5b49dbb1a8b30a07e580f316e
public_suffix (6.0.2) sha256=bfa7cd5108066f8c9602e0d6d4114999a5df5839a63149d3e8b0f9c1d3558394
puffing-billy (4.0.1) sha256=d23ea4b2fd54d6d213a85c4a1f603459d9b1634d93ac012dab59d36f380e778a
puffing-billy (4.0.2) sha256=c983610f737e3847301f64cf41dccd9dcedcac9219af54d6eaf8178cbe48b3cc
puma (6.6.0) sha256=f25c06873eb3d5de5f0a4ebc783acc81a4ccfe580c760cfe323497798018ad87
puma-plugin-statsd (2.6.0) sha256=c13ffe6a2fa2d3c245af673316e6d2556f5d0c151455d1934dffc96ce814ea03
raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
rack (2.2.15) sha256=cfd082f748540702228e9d83f16e033be9c3a3e58f428f60f6bce26df38c1067
rack (2.2.17) sha256=5fe02a1ca80d6fb2271dba00985ee2962d6f5620b6f46dfed89f5301ac4699dd
rack-attack (6.7.0) sha256=3ca47e8f66cd33b2c96af53ea4754525cd928ed3fa8da10ee6dad0277791d77c
rack-cors (2.0.2) sha256=415d4e1599891760c5dc9ef0349c7fecdf94f7c6a03e75b2e7c2b54b82adda1b
rack-mini-profiler (3.3.1) sha256=2bf0de7d5795f54581e453b248e42cc50e8d0529efac73828653a9ad2407a801
rack-mini-profiler (4.0.0) sha256=c37bedcb7d01e33ad4addd8c4e742986e75db7cd8908cba3432c60b4e812e00f
rack-oauth2 (2.2.1) sha256=c73aa87c508043e2258f02b4fb110cacba9b37d2ccf884e22487d014a120d1a5
rack-protection (3.2.0) sha256=3c74ba7fc59066453d61af9bcba5b6fe7a9b3dab6f445418d3b391d5ea8efbff
rack-session (1.0.2) sha256=a02115e5420b4de036839b9811e3f7967d73446a554b42aa45106af335851d76
@@ -1878,17 +1878,17 @@ CHECKSUMS
rackup (1.0.1) sha256=ba86604a28989fe1043bff20d819b360944ca08156406812dca6742b24b3c249
rails (8.0.2) sha256=fdfaa5a83ec0388e02864e88d515959caedc88053b5f701c4deb1652d8f164c6
rails-controller-testing (1.0.5) sha256=741448db59366073e86fc965ba403f881c636b79a2c39a48d0486f2607182e94
rails-dom-testing (2.2.0) sha256=e515712e48df1f687a1d7c380fd7b07b8558faa26464474da64183a7426fa93b
rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d
rails-html-sanitizer (1.6.2) sha256=35fce2ca8242da8775c83b6ba9c1bcaad6751d9eb73c1abaa8403475ab89a560
rails-i18n (8.0.1) sha256=15303195450bdac9a80636cf14c7e5ada2f43907cc60fcd19bbb3f81ab45be0d
railties (8.0.2) sha256=0d7c3f40c49ba74980f1bac1d4bb153a9331c5ee8a9631d89c7bf79db82e5cf9
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
rake (13.2.1) sha256=46cb38dae65d7d74b6020a4ac9d48afed8eb8149c040eccf0523bec91907059d
rake (13.3.0) sha256=96f5092d786ff412c62fde76f793cc0541bd84d2eb579caa529aa8a059934493
rake-compiler-dock (1.9.1) sha256=e73720a29aba9c114728ce39cc0d8eef69ba61d88e7978c57bac171724cd4d53
rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe
rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e
rb_sys (0.9.115) sha256=4696a623b89131f4a12bd9e38710add1c64b478dc3d2e9275f61a65f42f743ec
rbtrace (0.5.1) sha256=e8cba64d462bfb8ba102d7be2ecaacc789247d52ac587d8003549d909cb9c5dc
rb_sys (0.9.116) sha256=c879891018535d4362455197065ea580541b10ffdfa940bec512ec1dd9a7def4
rbtrace (0.5.2) sha256=a2d7d222ab81363aaa0e91337ddbf70df834885d401a80ea0339d86c71f31895
rbtree3 (0.7.1) sha256=ab60ead728a5491b70df4f4065e180b18dbab5319f817ce1dbf5dd906f26d8ba
rdoc (6.14.0) sha256=2c46de58d7129b8743fcf6d76e3db971bdc914150e15ac06b386549bd82ed7db
recaptcha (5.19.0) sha256=b69c021a5b788da06901d2c95ab86f10a8634e6496343096cc5167f0318b2a39
@@ -1914,8 +1914,8 @@ CHECKSUMS
rspec-retry (0.6.2) sha256=6101ba23a38809811ae3484acde4ab481c54d846ac66d5037ccb40131a60d858
rspec-support (3.13.4) sha256=184b1814f6a968102b57df631892c7f1990a91c9a3b9e80ef892a0fc2a71a3f7
rspec-wait (1.0.1) sha256=50ce9cc7cd20b8bb67b95a4ed56b59a16d4af7e86ae91b28dbf20eeaa2481d1f
rubocop (1.75.8) sha256=c80ab4286c5dcfc49d7ad1787cdba5569b63b58c96ee7afde4ec47a9c8a85be9
rubocop-ast (1.44.1) sha256=e3cc04203b2ef04f6d6cf5f85fe6d643f442b18cc3b23e3ada0ce5b6521b8e92
rubocop (1.76.1) sha256=e15a2d750794cf2157d2de8b1b403dfa71b8dc3957a22ae6043b1bdf21e7e0e7
rubocop-ast (1.45.1) sha256=94042e49adc17f187ba037b33f941ba7398fede77cdf4bffafba95190a473a3e
rubocop-capybara (2.22.1) sha256=ced88caef23efea53f46e098ff352f8fc1068c649606ca75cb74650970f51c0c
rubocop-factory_bot (2.27.1) sha256=9d744b5916778c1848e5fe6777cc69855bd96548853554ec239ba9961b8573fe
rubocop-openproject (0.2.0) sha256=2300ed6b638a34a9368b458dc5fb618ae396da6a27670b50519ad1e3cea65ccb
@@ -1971,7 +1971,7 @@ CHECKSUMS
tpm-key_attestation (0.14.0) sha256=d05cc52b397f89c36a7307407e0e84d3ea1c7afce50e0a70b146f8ab17d2bf4b
trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3
ttfunk (1.7.0) sha256=2370ba484b1891c70bdcafd3448cfd82a32dd794802d81d720a64c15d3ef2a96
turbo-rails (2.0.13) sha256=c40ac0a3ccd57c129925c8ac524a5dfd1e17fad080906e2d32135721a8bba22f
turbo-rails (2.0.16) sha256=d24e1b60f0c575b3549ecda967e5391027143f8220d837ed792c8d48ea0ea38d
turbo_power (0.7.0) sha256=ad95d147e0fa761d0023ad9ca00528c7b7ddf6bba8ca2e23755d5b21b290d967
turbo_tests (2.2.0)
typed_dag (2.0.2) sha256=b8dc65bf8cb5461715aa64650b7c96fc2075ef29fdd94554a93896c43453828c
@@ -1985,7 +1985,7 @@ CHECKSUMS
validate_email (0.1.6) sha256=9dfe9016d527b17a8d3a6e95e4dc50a125400eef899d13d4cc2a254393f82ee4
validate_url (1.0.15) sha256=72fe164c0713d63a9970bd6700bea948babbfbdcec392f2342b6704042f57451
vcr (6.3.1) sha256=37b56e157e720446a3f4d2d39919cabef8cb7b6c45936acffd2ef8229fec03ed
vernier (1.7.1) sha256=3cb17d41a43f32a317d76bd8973eb94d2610e65c5442ebdec6accbfb0ed734a4
vernier (1.8.0) sha256=c6cf9555c2a58affc25ebc45de0b4d45a993eb078f88f14f5b45d7bacb25acdd
view_component (3.23.2) sha256=3c2fed4b9e38bf074fa3d42ca55eedbb2cc070e0f3c31d7c13a50b0db530892b
virtus (2.0.0) sha256=8841dae4eb7fcc097320ba5ea516bf1839e5d056c61ee27138aa4bddd6e3d1c2
warden (1.2.9) sha256=46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0
@@ -1995,10 +1995,10 @@ CHECKSUMS
webmock (3.25.1) sha256=ab9d5d9353bcbe6322c83e1c60a7103988efc7b67cd72ffb9012629c3d396323
webrick (1.9.1) sha256=b42d3c94f166f3fb73d87e9b359def9b5836c426fc8beacf38f2184a21b2a989
websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737
websocket-driver (0.7.7) sha256=056d99f2cd545712cfb1291650fde7478e4f2661dc1db6a0fa3b966231a146b4
websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962
websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
will_paginate (4.0.1) sha256=107b226ebe1d393d274575956a7c472e1eefdd97d8828e01b72d425d15a875b9
with_advisory_lock (5.1.0) sha256=0692cd82013b271c59aa5e1f603b741ab94926dbce098990fbace5dd5412fed7
with_advisory_lock (5.3.0) sha256=bdb1864430853fe9081a7765f2d5b7cc42725c9ccf1901d20e9989828901b94e
xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e
yabeda (0.13.1) sha256=3213025f22b7746602c8a4c41e2ed82d73a90bdc5489f6ef472142b06c1cf954
yabeda-activerecord (0.1.1) sha256=ae338213c264f20d8642e8bf47ac6058c49a6f7c8c00c892cd5765332914f45f
+2
View File
@@ -4,6 +4,8 @@
@import "op_primer/border_box_table_component"
@import "op_primer/form_helpers"
@import "open_project/common/attribute_component"
@import "open_project/common/attribute_help_text_component"
@import "open_project/common/attribute_label_component"
@import "open_project/common/submenu_component"
@import "projects/row_component"
@import "projects/phases/hover_card_component"
@@ -98,7 +98,7 @@ module Admin
tag: :a,
content_arguments: { data: { turbo_frame: ItemsComponent.wrapper_key } },
href: new_child_custom_field_item_path(@root.custom_field_id, model.parent, position: model.sort_order)
) { _1.with_leading_visual_icon(icon: "fold-up") }
) { it.with_leading_visual_icon(icon: "fold-up") }
end
def add_below_action_item(menu)
@@ -107,16 +107,18 @@ module Admin
tag: :a,
content_arguments: { data: { turbo_frame: ItemsComponent.wrapper_key } },
href: new_child_custom_field_item_path(@root.custom_field_id, model.parent, position: model.sort_order + 1)
) { _1.with_leading_visual_icon(icon: "fold-down") }
) { it.with_leading_visual_icon(icon: "fold-down") }
end
def add_sub_item_action_item(menu)
position = model.children.any? ? model.children.maximum(:sort_order) + 1 : 0
menu.with_item(
label: I18n.t(:button_add_sub_item),
tag: :a,
content_arguments: { data: { turbo_frame: ItemsComponent.wrapper_key } },
href: new_child_custom_field_item_path(@root.custom_field_id, model)
) { _1.with_leading_visual_icon(icon: "op-arrow-in") }
href: new_child_custom_field_item_path(@root.custom_field_id, model, position:)
) { it.with_leading_visual_icon(icon: "op-arrow-in") }
end
def move_to_top_action_item(menu)
@@ -60,7 +60,7 @@ module Admin
end
def item_header
render(Primer::Beta::Breadcrumbs.new) do |loaf|
render(Primer::Beta::Breadcrumbs.new(data: { test_selector: "hierarchy-breadcrumbs" })) do |loaf|
slices.each do |slice|
loaf.with_item(href: slice[:href], target: nil) { slice[:label] }
end
@@ -0,0 +1,35 @@
<%#-- 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::Beta::Heading.new(tag: :h3, font_size: 4, mb: 2)) { @custom_field.name } %>
<%=
render(Primer::OpenProject::TreeView.new(node_variant: :anchor)) do |tree_view|
add_sub_tree(tree_view, hierarchy_items)
end
%>
@@ -0,0 +1,76 @@
# 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.
#++
module Admin
module CustomFields
module Hierarchy
class TreeViewComponent < ApplicationComponent
def initialize(custom_field:, active_item:)
super
@custom_field = custom_field
@active_item = active_item
@hierarchy_service = ::CustomFields::Hierarchy::HierarchicalItemService.new
end
def hierarchy_items
hashed_hierarchy = @custom_field.hierarchy_root.hash_tree
hashed_hierarchy.nil? ? {} : hashed_hierarchy.first[1]
end
def add_sub_tree(tree, hierarchy_hash)
hierarchy_hash.each do |item, child_hash|
if child_hash.empty?
tree.with_leaf(**item_options(item))
else
expanded = child_hash.any? { |child, _| current?(child) }
tree.with_sub_tree(expanded: expanded, **item_options(item)) do |sub_tree|
add_sub_tree(sub_tree, child_hash)
end
end
end
end
def item_options(item)
{
label: item.label,
current: current?(item),
href: custom_field_item_path(@custom_field, item)
}
end
def current?(item)
item.id == @active_item.id
end
end
end
end
end
+15
View File
@@ -42,6 +42,21 @@ class ApplicationComponent < ViewComponent::Base
end
class << self
##
# Generates a unique identifier suitable for use as an HTML `id` attribute
# value. If passed a non-nil model instance, it will use the model's id to
# generate the identifier (see {ActiveModel::Conversion#to_param}).
# Otherwise, it will generate an identifier with random numbers.
#
# @see ActiveModel::Conversion#to_param
#
# @param [ActiveModel::Model] model an optional model instance.
# @param [String] base_name a prefix for the unique identifier.
# @return [String] a unique identifier.
def generate_id(model = nil, base_name: name.demodulize.underscore.dasherize)
"#{base_name}-#{model&.to_param || SecureRandom.uuid}"
end
##
# Defines options for this cell which can be used within the cell's template.
# Options are passed to the cell during the render call.
@@ -0,0 +1,33 @@
<%=
render(Primer::Alpha::Dialog.new(id: dialog_id, title:, size: :large)) do |dialog|
dialog.with_header(variant: :large)
dialog.with_body do
concat(
render(Primer::Box.new(mt: 1, classes: "op-uc-container op-uc-container__no-permalinks")) do
helpers.format_text(@attribute_help_text, :help_text)
end
)
if has_attachments?
concat render(Primer::Beta::Heading.new(tag: :h4)) { I18n.t(:label_attachment_plural) }
concat helpers.list_attachments(resource_representer, inputs: { allowUploading: false })
end
end
dialog.with_footer(show_divider: true) do
if allowed_to_edit?
concat(
render(Primer::Beta::Button.new(tag: :a, href: edit_button_href)) do |button|
button.with_leading_visual_icon(icon: :pencil)
I18n.t(:button_edit)
end
)
end
concat(
render(Primer::Beta::Button.new(data: { close_dialog_id: dialog_id })) { I18n.t(:button_close) }
)
end
end
%>
@@ -0,0 +1,57 @@
# 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.
# ++
class AttributeHelpTexts::ShowDialogComponent < ApplicationComponent
include OpTurbo::Streamable
DIALOG_ID = "attribute-help-text-show-modal"
def initialize(attribute_help_text:, current_user: User.current)
super
@attribute_help_text = attribute_help_text
@current_user = current_user
end
private
def dialog_id = DIALOG_ID
def title = @attribute_help_text.attribute_caption
def has_attachments? = @attribute_help_text.attachments.any?
def allowed_to_edit? = @current_user.allowed_globally?(:edit_attribute_help_texts)
def edit_button_href = url_helpers.edit_attribute_help_text_path(@attribute_help_text)
def resource_representer
::API::V3::HelpTexts::HelpTextRepresenter.new(@attribute_help_text, current_user: @current_user, embed_links: false)
end
end
@@ -44,7 +44,7 @@ module EnterpriseEdition
VARIANT_OPTIONS = %i[inline medium large].freeze
# @param feature_key [Symbol, NilClass] The key of the feature to show the banner for.
# @param variant [Symbol, NilClass] The variant of the banner comopnent.
# @param variant [Symbol, NilClass] The variant of the banner component.
# @param image [String, NilClass] Path to the image to show on the banner, or nil.
# Only applicable and required when variant is :medium.
# @param video [String, NilClass] Path to the video to show on the banner, or nil.
@@ -55,7 +55,7 @@ module EnterpriseEdition
# @param show_always [boolean] Always show the banner, regardless of the dismissed or feature state.
# @param dismiss_key [String] Provide a string to identify this banner when being dismissed. Defaults to feature_key
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
def initialize(feature_key, # rubocop:disable Metrics/AbcSize
def initialize(feature_key,
variant: DEFAULT_VARIANT,
image: nil,
video: nil,
@@ -68,12 +68,15 @@ module EnterpriseEdition
@image = image
@video = video
@dismissable = dismissable
@dismiss_key = dismiss_key
@dismiss_key = dismiss_key.to_s
@show_always = show_always
self.feature_key = feature_key
self.i18n_scope = i18n_scope
trial_overrides! if trial_feature?
if @variant == :medium && @image.nil?
raise ArgumentError, "The 'image' parameter is required when the variant is :medium."
end
@@ -82,17 +85,7 @@ module EnterpriseEdition
raise ArgumentError, "The 'video' parameter is required when the variant is :large."
end
@system_arguments = system_arguments
@system_arguments[:tag] = :div
@system_arguments[:mb] ||= 2
@system_arguments[:id] = "op-enterprise-banner-#{feature_key.to_s.tr('_', '-')}"
@system_arguments[:test_selector] = "op-enterprise-banner"
@system_arguments[:classes] = class_names(
@system_arguments[:classes],
"op-enterprise-banner",
@variant == :medium ? "op-enterprise-banner_medium" : nil,
@variant == :large ? "op-enterprise-banner_large" : nil
)
set_system_arguments(system_arguments, feature_key)
super
end
@@ -115,15 +108,39 @@ module EnterpriseEdition
end
def wrapper_key
"enterprise_banner_#{feature_key}"
"enterprise_banner_#{@dismiss_key}"
end
private
def set_system_arguments(system_arguments, feature_key)
@system_arguments = system_arguments
@system_arguments[:tag] = :div
@system_arguments[:mb] ||= 2
@system_arguments[:id] = "op-enterprise-banner-#{feature_key.to_s.tr('_', '-')}"
@system_arguments[:test_selector] = "op-enterprise-banner"
@system_arguments[:classes] = class_names(
@system_arguments[:classes],
"op-enterprise-banner",
"op-enterprise-banner_medium" => @variant == :medium,
"op-enterprise-banner_large" => @variant == :large,
"op-enterprise-banner_trial" => trial_feature?
)
end
def trial_overrides!
@dismissable = true
@dismiss_key += "_trial" unless @dismiss_key.end_with?("_trial")
@variant = :inline
end
def render?
return true if @show_always
return false if dismissed?
return true if feature_available? && trial_feature?
return false if EnterpriseToken.hide_banners?
!(EnterpriseToken.hide_banners? || feature_available? || dismissed?)
!feature_available?
end
def feature_available?
@@ -135,5 +152,9 @@ module EnterpriseEdition
User.current.pref.dismissed_banner?(@dismiss_key)
end
def trial_feature?
EnterpriseToken.trialling?(feature_key)
end
end
end
@@ -58,6 +58,7 @@ module EnterpriseEdition
def buttons
[
buy_now_button,
free_trial_button,
upgrade_now_button,
more_info_button
@@ -74,6 +75,20 @@ module EnterpriseEdition
)
end
def buy_now_button
return unless EnterpriseToken.active?
return unless User.current.admin?
render(Primer::Beta::Button.new(
classes: "upsell-colored-background",
tag: :a,
href: OpenProject::Enterprise.upgrade_path,
align_self: :center
)) do
I18n.t("ee.upsell.buy_now_button")
end
end
# Allow providing a custom upgrade now button
def upgrade_now_button
nil
@@ -0,0 +1,6 @@
<%=
render Primer::Beta::Link.new(**@system_arguments) do
render Primer::Beta::Octicon.new(:question, size: :xsmall)
end
%>
<%= render @tooltip %>
@@ -0,0 +1,71 @@
# 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.
#++
module OpenProject
module Common
class AttributeHelpTextComponent < ApplicationComponent
def initialize(help_text:, **system_arguments) # rubocop:disable Metrics/AbcSize
super()
@help_text = help_text
@system_arguments = system_arguments
@system_arguments[:id] ||= self.class.generate_id(help_text)
@system_arguments[:classes] = class_names(
@system_arguments[:classes],
"op-attribute-help-text"
)
@system_arguments[:data] ||= {}
@system_arguments[:data][:controller] = "async-dialog"
@tooltip = Primer::Alpha::Tooltip.new(
for_id: @system_arguments[:id],
type: :label,
text: I18n.t("js.help_texts.show_modal"),
direction: :sw
)
@system_arguments[:aria] ||= {}
@system_arguments[:aria][:labelledby] = @tooltip.id
end
private
def render?
@help_text.present?
end
def before_render
return unless @help_text
@system_arguments[:href] = show_dialog_attribute_help_text_path(@help_text)
end
end
end
end
@@ -0,0 +1,30 @@
//-- 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.
//++
.op-attribute-help-text
line-height: 1rem
@@ -0,0 +1,4 @@
<%= render Primer::ConditionalWrapper.new(condition: !@tag.nil?, trim: true, **@system_arguments) do %>
<%= content %>
<%= render OpenProject::Common::AttributeHelpTextComponent.new(help_text: @help_text) if @help_text.present? %>
<% end %>
@@ -0,0 +1,57 @@
# 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.
#++
module OpenProject
module Common
class AttributeLabelComponent < ApplicationComponent
def initialize(
attribute:,
model:,
tag: :span,
current_user: User.current,
**system_arguments
)
super()
@tag = tag
@system_arguments = system_arguments
@system_arguments[:tag] = @tag
@system_arguments[:classes] = class_names(
@system_arguments[:classes],
"op-attribute-label"
)
@help_text = ::AttributeHelpText.for(model)
&.cached(current_user)
&.[](attribute.to_s)
end
end
end
end
@@ -0,0 +1,34 @@
//-- 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.
//++
.op-attribute-label
display: inline-flex
> .op-attribute-help-text
align-self: baseline
margin-left: var(--base-size-6)
@@ -78,7 +78,7 @@
data: { "projects--settings--border-box-filter-target": "searchItem" },
test_selector: "project-life-cycle-step-#{definition.id}"
) do
render(Projects::Settings::LifeCycleSteps::StepComponent.new(definition:, active?: active))
render(Projects::Settings::LifeCycle::PhaseComponent.new(definition:, active?: active))
end
end
end
@@ -28,7 +28,7 @@
module Projects
module Settings
module LifeCycleSteps
module LifeCycle
class IndexComponent < ApplicationComponent
include ApplicationHelper
include OpPrimer::ComponentHelpers
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
# ++
module Projects::Settings::LifeCycleSteps
module Projects::Settings::LifeCycle
class IndexPageHeaderComponent < ApplicationComponent
include ApplicationHelper
@@ -7,7 +7,7 @@
render(
Projects::PhaseDefinitionComponent.new(
definition,
edit_link: User.current.admin?,
edit_link: definition_editable?,
phase_text_options: { classes: "filter-target-visible-text" }
)
)
@@ -28,11 +28,12 @@
module Projects
module Settings
module LifeCycleSteps
class StepComponent < ApplicationComponent
module LifeCycle
class PhaseComponent < ApplicationComponent
include ApplicationHelper
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable
include Projects::LifeCycleDefinitionHelper
options :definition,
:active?
@@ -40,6 +41,10 @@ module Projects
def toggle_aria_label
I18n.t("projects.settings.life_cycle.step.use_in_project", step: definition.name)
end
def definition_editable?
User.current.admin? && allowed_to_customize_life_cycle?
end
end
end
end
@@ -31,7 +31,14 @@ See COPYRIGHT and LICENSE files for more details.
component_wrapper do
flex_layout(data: wrapper_data_attributes) do |flex|
flex.with_row do
if allowed_to_customize_life_cycle?
render EnterpriseEdition::BannerComponent.new(:customize_life_cycle,
variant: :medium,
image: "enterprise/project-lifecycle.png",
mb: 3)
end
if allowed_to_customize_life_cycle?
flex.with_row do
render(Primer::OpenProject::SubHeader.new) do |subheader|
subheader.with_filter_input(
name: "border-box-filter",
@@ -57,11 +64,6 @@ See COPYRIGHT and LICENSE files for more details.
I18n.t("settings.project_phase_definitions.label_add")
end
end
else
render EnterpriseEdition::BannerComponent.new(:customize_life_cycle,
variant: :medium,
image: "enterprise/project-lifecycle.png",
mb: 3)
end
end
@@ -7,7 +7,8 @@
end
end
render(strategy.manage_shares_component(modal_content:, errors:))
render(strategy.upsell_banner(modal_content:))
render(strategy.manage_shares_component(modal_content:, errors:)) if strategy.allow_feature?
end
end
%>
@@ -1,5 +1,7 @@
<%=
component_wrapper(tag: "turbo-frame") do
render(EnterpriseEdition::BannerComponent.new(:work_package_sharing))
modal_content.with_row do
render(EnterpriseEdition::BannerComponent.new(:work_package_sharing))
end
end
%>
@@ -33,6 +33,14 @@ module Shares
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
def initialize(modal_content:)
super
@modal_content = modal_content
end
attr_reader :modal_content
def self.wrapper_key
"share_modal_body"
end
@@ -36,15 +36,17 @@ module WorkPackages
include OpPrimer::ComponentHelpers
FORM_ID = "reminder-form"
DEFAULT_TIME = "09:00"
attr_reader :remindable, :reminder, :errors
attr_reader :remindable, :reminder, :errors, :preset
def initialize(remindable:, reminder:, errors: nil)
def initialize(remindable:, reminder:, errors: nil, preset: nil)
super
@remindable = remindable
@reminder = reminder
@errors = errors
@preset = preset
end
class << self
@@ -55,9 +57,9 @@ module WorkPackages
def submit_path
if @reminder.persisted?
work_package_reminder_path(@remindable, @reminder)
url_helpers.work_package_reminder_path(@remindable, @reminder)
else
work_package_reminders_path(@remindable)
url_helpers.work_package_reminders_path(@remindable)
end
end
@@ -82,11 +84,32 @@ module WorkPackages
end
def remind_at_date_initial_value
format_time_as_date(@reminder.remind_at, format: "%Y-%m-%d")
return time_as_date(@reminder.remind_at) if @reminder.remind_at
return calculate_preset_date if @preset
nil
end
def remind_at_time_initial_value
format_time(@reminder.remind_at, include_date: false, format: "%H:%M")
return format_time(@reminder.remind_at, include_date: false, format: "%H:%M") if @reminder.remind_at
DEFAULT_TIME
end
private
def calculate_preset_date
case @preset
when "tomorrow" then time_as_date(1.day.from_now)
when "three_days" then time_as_date(3.days.from_now)
when "week" then time_as_date(7.days.from_now)
when "month" then time_as_date(1.month.from_now)
when "custom" then nil
end
end
def time_as_date(time)
format_time_as_date(time, format: "%Y-%m-%d")
end
end
end
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module ProjectLifeCycleSteps
module ProjectPhases
class ActivationContract < ::ModelContract
alias_method :project, :model
@@ -26,7 +26,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module ProjectLifeCycleSteps
module ProjectPhases
class BaseContract < ::ModelContract
validate :validate_edit_project_phases_permission
@@ -0,0 +1,41 @@
# 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.
#++
module ProjectPhases
class RescheduleContract < BaseContract
alias_method :project, :model
protected
def validate_model?
false
end
end
end
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module ProjectLifeCycleSteps
module ProjectPhases
class UpdateContract < BaseContract
validate :validate_start_after_preceeding_phases
validate :validate_start_date_is_a_working_day
+1 -1
View File
@@ -69,7 +69,7 @@ module Queries
end
def project_visible?
Project.visible(user).exists?(id: project_id)
::Project.visible(user).exists?(id: project_id)
end
def may_not_manage_queries?
@@ -66,8 +66,15 @@ module Settings
end
def unique_job
WorkPackages::ApplyWorkingDaysChangeJob.new.check_concurrency do
errors.add :base, :previous_working_day_changes_unprocessed
[
Projects::Phases::ApplyWorkingDaysChangeJob,
WorkPackages::ApplyWorkingDaysChangeJob
].each do |job_class|
job_class.new.check_concurrency do
errors.add :base, :previous_working_day_changes_unprocessed
end
break if errors.added? :base, :previous_working_day_changes_unprocessed
end
end
@@ -37,10 +37,19 @@ module WorkPackages
attribute :done_ratio,
writable: true
# Use the default permission for the create contract, which is :add_work_packages.
attribute_permission :project_phase_definition_id, :add_work_packages
# Do not validate predecessors or children presence when copying: when
# copying, it's possible to create a work package in automatic scheduling
# mode even if it has no predecessors or children yet. They will be added
# later in the process.
def validate_has_predecessors_or_children; end
# No validation happening on whether the phase is active in the project.
# When copying a work package, e.g. from a project template, the phase
# might not be active in the project yet. But when it is activated later,
# the value should then be present.
def validate_phase_active_in_project; end
end
end
@@ -27,17 +27,29 @@
#++
class AttributeHelpTextsController < ApplicationController
include OpTurbo::ComponentStream
include OpTurbo::DialogStreamHelper
layout "admin"
menu_item :attribute_help_texts
before_action :authorize_global
before_action :authorize_global, except: :show_dialog
before_action :find_entry, only: %i(edit update destroy)
before_action :find_type_scope
authorization_checked! :show_dialog
def index
@texts_by_type = AttributeHelpText.all_by_scope
end
def show_dialog
@attribute_help_text = AttributeHelpText.visible(current_user).find(params[:id])
respond_with_dialog(
AttributeHelpTexts::ShowDialogComponent.new(attribute_help_text: @attribute_help_text)
)
end
def new
@attribute_help_text = AttributeHelpText.new type: @attribute_scope
end
@@ -47,9 +47,13 @@ class My::EnterpriseBannersController < ApplicationController
def dismiss
pref = User.current.pref
pref.dismiss_banner(@feature_key)
pref.dismiss_banner(@dismiss_key)
if pref.save
remove_via_turbo_stream(component: EnterpriseEdition::BannerComponent.new(@feature_key))
remove_via_turbo_stream(component: EnterpriseEdition::BannerComponent.new(
@feature_key,
dismiss_key: @dismiss_key,
show_always: true
))
respond_with_turbo_streams
else
respond_with_flash_error(message: call.message)
@@ -59,7 +63,11 @@ class My::EnterpriseBannersController < ApplicationController
private
def get_feature_key
@feature_key = params[:feature_key].to_sym
raw_key = params[:feature_key]
@dismiss_key = raw_key
@feature_key = raw_key.gsub(/_trial$/, "").to_sym
render_400 unless OpenProject::Token.lowest_plan_for(@feature_key)
end
end
@@ -66,7 +66,7 @@ class Projects::Settings::LifeCycleStepsController < Projects::SettingsControlle
end
def set_steps_active_status(definitions, active:)
::ProjectLifeCycleSteps::ActivationService
::ProjectPhases::ActivationService
.new(user: current_user, project: @project, definitions:)
.call(active:)
end
@@ -40,7 +40,8 @@ class WorkPackages::RemindersController < ApplicationController
def modal_body
render modal_component_class.new(
remindable: @work_package,
reminder: @reminder
reminder: @reminder,
preset: params[:preset]
)
end
+3 -3
View File
@@ -29,6 +29,8 @@
#++
class ApplicationForm < Primer::Forms::Base
include AttributeHelpTexts::FormHelper
def self.settings_form
form do |f|
f = Settings::FormDecorator.new(f)
@@ -41,9 +43,7 @@ class ApplicationForm < Primer::Forms::Base
end
# @return [ActionView::Base] the view helper instance
def helpers
@view_context.helpers
end
delegate :helpers, to: :@view_context
# @return [ActiveRecord::Base] the model instance given to the form builder
def model
@@ -0,0 +1,42 @@
# 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.
#++
module AttributeHelpTexts
module FormHelper
def wrap_attribute_label_with_help_text(label, attribute)
render OpenProject::Common::AttributeLabelComponent.new(
model:,
attribute:,
tag: nil,
current_user: User.current
).with_content(label)
end
end
end
+11 -6
View File
@@ -92,12 +92,17 @@ module Projects::LifeCycles
end
def autofocus?(field_name)
start_date_blank = model.start_date.blank? && model.default_start_date.blank?
case field_name
when :start_date
start_date_blank
when :finish_date
!start_date_blank && model.finish_date.blank?
# let javascipt handle focusing when rendering for preview
return false if model.changed?
field_name == autofocus_field_name
end
def autofocus_field_name
if start_date_disabled? || (model.start_date? && !model.finish_date?)
:finish_date
else
:start_date
end
end
+2 -4
View File
@@ -86,10 +86,8 @@ module Statuses
}
)
if readonly_work_packages_restricted?
statuses_form.html_content do
render(EnterpriseEdition::BannerComponent.new(:readonly_work_packages))
end
statuses_form.html_content do
render(EnterpriseEdition::BannerComponent.new(:readonly_work_packages))
end
statuses_form.check_box(
+2 -2
View File
@@ -116,9 +116,9 @@ module RepositoriesHelper
def render_changes_tree(tree)
return "" if tree.nil?
output = "<ul>"
output = +"<ul>"
tree.keys.sort.each do |file|
style = "change"
style = +"change"
text = File.basename(file)
if s = tree[file][:s]
style << " folder"
+8
View File
@@ -29,6 +29,14 @@
class AttributeHelpText < ApplicationRecord
acts_as_attachable viewable_by_all_users: true
def self.cached(user)
OpenProject::Cache.fetch([name, user]) { visible(user).select(:id, :attribute_name).index_by(&:attribute_name) }
end
def self.for(model)
subclasses.find { |child| child.name.demodulize == model.model_name }
end
def self.available_types
subclasses.map { |child| child.name.demodulize }
end
+1 -1
View File
@@ -47,7 +47,7 @@ class AttributeHelpText::Project < AttributeHelpText
validates :attribute_name, inclusion: { in: ->(*) { available_attributes.keys } }
def type_caption
Project.model_name.human
::Project.model_name.human
end
def self.visible_condition(_user)
+13
View File
@@ -45,6 +45,18 @@ class EnterpriseToken < ApplicationRecord
current && !current.expired?
end
def available_features
EnterpriseToken.current&.available_features || []
end
def trialling_features
available_features.select { |feature| trialling?(feature) }
end
def trialling?(feature)
allows_to?(feature) && EnterpriseToken.current.trial?
end
def hide_banners?
OpenProject::Configuration.ee_hide_banners?
end
@@ -88,6 +100,7 @@ class EnterpriseToken < ApplicationRecord
:plan,
:features,
:version,
:trial?,
to: :token_object
def token_object
+1
View File
@@ -139,6 +139,7 @@ class Project < ApplicationRecord
datetime: :created_at
register_journal_formatted_fields "active", formatter_key: :active_status
register_journal_formatted_fields "cause", formatter_key: :cause
register_journal_formatted_fields "templated", formatter_key: :template
register_journal_formatted_fields "identifier", "name", formatter_key: :plaintext
register_journal_formatted_fields "status_explanation", "description", formatter_key: :diff
+8 -7
View File
@@ -51,7 +51,8 @@ class Project::Phase < ApplicationRecord
attr_readonly :definition_id
scope :active, -> { where(active: true) }
scopes :order_by_position
scopes :order_by_position,
:covering_dates_or_days_of_week
class << self
def visible(user = User.current)
@@ -85,24 +86,24 @@ class Project::Phase < ApplicationRecord
end
def follows_previous_phase?
previous_finish_dates.last.present?
!!previous_phase&.date_range_set?
end
def default_start_date
return @default_start_date if defined?(@default_start_date)
previous_finish_date = previous_finish_dates.compact.last
previous_finish_date = previous_phase&.finish_date
@default_start_date = previous_finish_date && Day.next_working(from: previous_finish_date).date
end
private
def previous_finish_dates
return @previous_finish_dates if defined?(@previous_finish_dates)
def previous_phase
return @previous_phase if defined?(@previous_phase)
@previous_finish_dates = project
@previous_phase = project
.available_phases
.select { it.position < position }
.map(&:finish_date)
.last
end
end
+2
View File
@@ -48,4 +48,6 @@ class Project::PhaseDefinition < ApplicationRecord
default_scope { order(:position) }
scopes :with_project_count
def to_s; name end
end
@@ -0,0 +1,88 @@
# 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.
#++
module Project::Phases::Scopes::CoveringDatesOrDaysOfWeek
extend ActiveSupport::Concern
using CoreExtensions::SquishSql
class_methods do
# Fetches all phases that cover specific days of the week, and/or specific dates.
#
# The period considered is from the phase start date to the due date.
#
# @param dates Date[] An array of the Date objects.
# @param days_of_week number[] An array of the ISO days of the week to
# consider. 1 is Monday, 7 is Sunday.
def covering_dates_or_days_of_week(days_of_week: [], dates: [])
days_of_week = Array(days_of_week)
dates = Array(dates)
return none if days_of_week.empty? && dates.empty?
ids_sql = sanitize_sql([<<~SQL.squish, { days_of_week:, dates: }])
WITH
-- select phases with at least one date
phases_with_dates AS (
SELECT
id,
COALESCE(start_date, finish_date) AS start_date,
COALESCE(finish_date, start_date) AS finish_date
FROM project_phases
WHERE
start_date IS NOT NULL
OR finish_date IS NOT NULL
)
SELECT
id
FROM
phases_with_dates
WHERE
-- Check if the range covers any of the provided days of week. It is
-- done by comparing number of days from start_date to first target
-- day of week with number of days in range. If number is less or
-- eqal, then the range covers the target day of week
EXISTS (
SELECT 1
FROM UNNEST(ARRAY[:days_of_week]::INT[]) AS target_dow
WHERE (target_dow + 7 - EXTRACT(ISODOW FROM start_date)::INT) % 7 <= finish_date - start_date
)
OR
-- Check if the range covers any of the provided dates
EXISTS (
SELECT 1
FROM UNNEST(ARRAY[:dates]::DATE[]) AS target_date
WHERE target_date BETWEEN start_date AND finish_date
)
SQL
where("id IN (#{ids_sql})")
end
end
end
+43
View File
@@ -0,0 +1,43 @@
# 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.
#++
module Queries
module PhaseDefinitions
# intentionally left empty, just exists to satisfy the autoloader
end
module Project::PhaseDefinitions
# This is needed for the phase definition filter to work, even though it is referenced in the
# WorkPackages::Query
::Queries::Register.register(PhaseDefinitionQuery) do
# intentionally left empty
end
end
end
@@ -0,0 +1,46 @@
# 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.
#++
module Queries::Project
module PhaseDefinitions
class PhaseDefinitionQuery
include Queries::BaseQuery
include Queries::UnpersistedQuery
def self.model
Project::PhaseDefinition
end
def default_scope
Project::PhaseDefinition.order(:position)
end
end
end
end
@@ -0,0 +1,44 @@
# 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.
#++
module Queries::ProjectPhases
class PhaseQuery
include Queries::BaseQuery
include Queries::UnpersistedQuery
def self.model
Project::Phase
end
def default_scope
Project::Phase.visible(User.current)
end
end
end
+2
View File
@@ -62,6 +62,7 @@ module Queries::WorkPackages
filter Filter::DatesIntervalFilter
filter Filter::ParentFilter
filter Filter::PrecedesFilter
filter Filter::ProjectPhaseFilter
filter Filter::FollowsFilter
filter Filter::RelatesFilter
filter Filter::DuplicatesFilter
@@ -84,6 +85,7 @@ module Queries::WorkPackages
exclude Filter::RelatableFilter
select Selects::PropertySelect
select Selects::ProjectPhaseSelect
select Selects::CustomFieldSelect
select Selects::RelationToTypeSelect
select Selects::RelationOfTypeSelect
@@ -0,0 +1,109 @@
# 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.
#++
class Queries::WorkPackages::Filter::ProjectPhaseFilter < Queries::WorkPackages::Filter::WorkPackageFilter
def allowed_values
@allowed_values ||= project_phase_definitions.map { |s| [s.name, s.id.to_s] }
end
def available?
project_phase_definitions.any?
end
def type
:list
end
def self.key
:project_phase_definition_id
end
def ar_object_filter?
true
end
def value_objects
available_definitions = project_phase_definitions.index_by(&:id)
values.filter_map { |id| available_definitions[id.to_i] }
end
# TODO: copied over from ProjectPhaseSelect, refactor to use the same truth
def joins
<<~SQL.squish
LEFT OUTER JOIN "projects" ON "projects"."id" = "work_packages"."project_id"
LEFT OUTER JOIN (
SELECT
ph.id,
ph.project_id,
ph.definition_id AS active_phase_definition_id
FROM project_phases ph
WHERE ph.project_id IN (#{project_with_view_phases_permissions.to_sql})
AND ph.active = true
) AS active_phases
ON active_phases.active_phase_definition_id = work_packages.project_phase_definition_id
AND active_phases.project_id = work_packages.project_id
SQL
end
def project_with_view_phases_permissions
Project.allowed_to(User.current, :view_project_phases).select(:id)
end
def where
placeholders = values.map { "?" }.join(",")
sql = if operator_strategy.to_sym == :"="
<<~SQL.squish
active_phases.active_phase_definition_id IS NOT NULL AND
active_phases.active_phase_definition_id IN (#{placeholders})
SQL
else
<<~SQL.squish
active_phases.active_phase_definition_id IS NULL OR
active_phases.active_phase_definition_id NOT IN (#{placeholders})
SQL
end
ActiveRecord::Base.sanitize_sql_array([sql, *values])
end
private
def project_phase_feature_flag_enabled?
OpenProject::FeatureDecisions.stages_and_gates_active?
end
def project_phase_definitions
return Project::PhaseDefinition.none unless project_phase_feature_flag_enabled?
Project::PhaseDefinition.order(Arel.sql("position"))
end
end
@@ -0,0 +1,118 @@
# 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.
#++
class Queries::WorkPackages::Selects::ProjectPhaseSelect < Queries::WorkPackages::Selects::WorkPackageSelect
def initialize
super(:project_phase,
association: :project_phase_definition,
group_by_column_name: :project_phase_definition,
sortable: sortable_statement,
groupable: group_by_statement,
groupable_join: group_by_join_statement,
groupable_select: groupable_select
)
end
def groupable_select
"#{group_by_statement} as project_phase_definition_id"
end
def group_by_statement
active_phase_null_case(true_case: "NULL", false_case: "project_phase_definitions.id")
end
def order_for_count
active_phase_null_case(true_case: "1", false_case: "0")
end
# Is called when the query is grouped by project phase definition. We ensure that only active project phases are considered.
# Note that one project might have an active phase while another project has set the phase with the same definition to inactive.
# Additionally, the permissions to view project phases are considered on a project level, too.
def group_by_join_statement
# The project is joined here anew which should not be necessary but is.
# The necessity comes from AR's behaviour of automatically determining the alias for tables LEFT JOINed via includes.
# To avoid conflicts, AR will search strings for occurrences of the table name and if found, an included table will be aliased
# (potentially with a numbering). In this case, if the permission checks are part of the query, it will include a reference
# to the projects table. Therefore, the include for projects, which happens in the query itself, will be considered needing
# an alias. That assumption is wrong in this case as the reference to projects is in a subquery but AR does not know that.
<<~SQL.squish
LEFT OUTER JOIN "projects" ON "projects"."id" = "work_packages"."project_id"
LEFT OUTER JOIN (
SELECT
ph.id,
ph.project_id,
ph.definition_id AS active_phase_definition_id
FROM project_phases ph
WHERE ph.project_id IN (#{project_with_view_phases_permissions.to_sql})
AND ph.active = true
) AS active_phases
ON active_phases.active_phase_definition_id = work_packages.project_phase_definition_id
AND active_phases.project_id = work_packages.project_id
SQL
end
def sortable_join_statement(_query)
# Replicate the group by join to ensure the same conditions are applied (and the same alias for the join is used)
group_by_join_statement
end
def sortable_statement
# We use the join alias from the group by join statement to ensure that work packages with an *inactive* project
# phase are treated like work packages *without* a project phase. In the result list, they will belong to the
# same group: without an active project phase.
active_phase_null_case(true_case: "-1", false_case: "project_phase_definitions.position")
end
def self.instances(context = nil)
allowed = if context
OpenProject::FeatureDecisions.stages_and_gates_active? &&
User.current.allowed_in_project?(:view_project_phases, context)
else
OpenProject::FeatureDecisions.stages_and_gates_active? &&
User.current.allowed_in_any_project?(:view_project_phases)
end
if allowed
[new]
else
[]
end
end
private
def project_with_view_phases_permissions
Project.allowed_to(User.current, :view_project_phases).select(:id)
end
def active_phase_null_case(true_case:, false_case:)
"(CASE WHEN ACTIVE_PHASES.ID IS NULL THEN #{true_case} ELSE #{false_case} END)"
end
end
@@ -145,7 +145,7 @@ class Queries::WorkPackages::Selects::PropertySelect < Queries::WorkPackages::Se
}
def self.instances(_context = nil)
property_selects.map do |name, options|
property_selects.filter_map do |name, options|
new(name, options)
end
end
@@ -32,6 +32,7 @@ class Queries::WorkPackages::Selects::WorkPackageSelect
:sortable_join,
:groupable_join,
:groupable_select,
:group_by_column_name,
:summable,
:default_order,
:association,
@@ -129,6 +130,7 @@ class Queries::WorkPackages::Selects::WorkPackageSelect
summable_select
summable_work_packages_select
association
group_by_column_name
null_handling
default_order
].each do |attribute|
-5
View File
@@ -289,11 +289,6 @@ class Query < ApplicationRecord
.compact_blank
.map(&:to_sym)
# Set column_names to blank/nil if it is equal to the default columns
if col_names.map(&:to_s) == Setting.work_package_list_default_columns
col_names.clear
end
write_attribute(:column_names, col_names)
end
+15 -7
View File
@@ -158,13 +158,8 @@ module ::Query::Results::GroupBy
end
def transform_property_keys(groups)
association = WorkPackage.reflect_on_all_associations.detect { |a| a.name == query.group_by_column.name.to_sym }
if association
transform_association_property_keys(association, groups)
else
groups
end
association = find_association_for_group
association ? transform_association_property_keys(association, groups) : groups
end
def transform_association_property_keys(association, groups)
@@ -213,4 +208,17 @@ module ::Query::Results::GroupBy
"#{order} #{column.null_handling(order == 'asc')}"
end
def find_association_for_group
WorkPackage.reflect_on_all_associations.detect do |association|
matches_group_by_column?(association)
end
end
def matches_group_by_column?(association)
# Some query columns override their groupable column name, prefer that if given:
group_name = query.group_by_column.group_by_column_name || query.group_by_column.name
association.name == group_name.to_sym
end
end
+4
View File
@@ -45,6 +45,10 @@ class Reminder < ApplicationRecord
.where.missing(:reminder_notifications)
end
def visible?(user = User.current)
creator == user && remindable.visible?(user)
end
def unread_notifications?
unread_notifications.exists?
end
@@ -105,18 +105,19 @@ module SharingStrategies
end
end
def manage_shares_component(modal_content:, errors:)
if EnterpriseToken.allows_to?(:project_list_sharing)
super
else
Shares::ProjectQueries::UpsellComponent.new(modal_content:)
end
end
def title
I18n.t(:label_share_project_list)
end
def upsell_banner(modal_content:)
Shares::ProjectQueries::UpsellComponent.new(modal_content:)
end
def allow_feature?
EnterpriseToken.allows_to?(:project_list_sharing) ||
EnterpriseToken.trialling?(:project_list_sharing)
end
private
def virtual_owner_share
@@ -99,18 +99,19 @@ module SharingStrategies
Shares::WorkPackages::DeleteContract
end
def modal_body_component(errors)
if EnterpriseToken.allows_to?(:work_package_sharing)
super
else
Shares::WorkPackages::ModalUpsellComponent.new
end
end
def title
I18n.t(:label_share_work_package)
end
def upsell_banner(modal_content:)
Shares::WorkPackages::ModalUpsellComponent.new(modal_content:)
end
def allow_feature?
EnterpriseToken.allows_to?(:work_package_sharing) ||
EnterpriseToken.trialling?(:work_package_sharing)
end
private
def project_member?(share)
+2 -2
View File
@@ -127,7 +127,7 @@ class WorkPackage < ApplicationRecord
where(author_id: author.id)
}
scopes :covering_dates_and_days_of_week,
scopes :covering_dates_or_days_of_week,
:allowed_to,
:for_scheduling,
:include_derived_dates,
@@ -335,7 +335,7 @@ class WorkPackage < ApplicationRecord
end
def duration_in_hours
duration ? duration * 24 : nil
duration * 24 if duration
end
def project_phase
@@ -63,19 +63,18 @@ module WorkPackage::Exports
end
def self.process_match(match, _matched_string, context)
context => { user:, work_package: }
type = match[2].downcase
model_s = match[1]
model_s = match[1].downcase
id = match[4] || match[3]
attribute = match[6] || match[5]
resolve_match(type, model_s, id, attribute, work_package, user)
resolve_match(type, model_s, id, attribute, context)
end
def self.resolve_match(type, model_s, id, attribute, work_package, user)
if model_s == "workPackage"
resolve_work_package_match(id || work_package.id, type, attribute, user)
def self.resolve_match(type, model_s, id, attribute, context)
if model_s == "workpackage"
resolve_work_package_match(id || context[:work_package]&.id, type, attribute, context[:user])
elsif model_s == "project"
resolve_project_match(id || work_package.project.id, type, attribute, user)
resolve_project_match(id || context[:project]&.id, type, attribute, context[:user])
else
msg_macro_error I18n.t("export.macro.model_not_found", model: model_s)
end
@@ -0,0 +1,61 @@
# 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.
#++
module WorkPackage::Exports
module Macros
class WorkPackagesLinkHandler < OpenProject::TextFormatting::Matchers::LinkHandlers::WorkPackages
def applicable?
%w(# ## ###).include?(matcher.sep) && matcher.prefix.blank?
end
def render_link(wp_id, matcher)
link = "#{matcher.sep}#{wp_id}"
"<mention class=\"mention\" data-id=\"#{wp_id}\" data-type=\"work_package\" data-text=\"#{link}\">#{
link
}</mention>"
end
end
class Links < OpenProject::TextFormatting::Matchers::ResourceLinksMatcher
def self.link_handlers
[WorkPackagesLinkHandler]
end
def self.html_replacement?
true
end
# Faster inclusion check before the full regex is being applied
def self.applicable?(content)
/#\d/.match(content)
end
end
end
end
@@ -30,21 +30,17 @@ require "mini_magick"
module WorkPackage::PDFExport::Common::Attachments
def resize_image(file_path)
tmp_file = Tempfile.new(["temp_image", File.extname(file_path)])
@resized_images = [] if @resized_images.nil?
@resized_images << tmp_file
resized_file_path = tmp_file.path
tmp_file = temp_image_file(File.extname(file_path))
image = MiniMagick::Image.open(file_path)
image.resize("x800>")
image.write(resized_file_path)
image.write(tmp_file)
resized_file_path
tmp_file
end
def pdf_embeddable?(content_type)
%w[image/jpeg image/png].include?(content_type)
%w[image/jpeg image/png image/gif].include?(content_type)
end
def delete_all_resized_images
@@ -59,18 +55,36 @@ module WorkPackage::PDFExport::Common::Attachments
nil # return nil as if the id was wrong and the attachment obj has not been found
end
def attachment_image_filepath(work_package, src)
def attachment_image_filepath(src)
# images are embedded into markup with the api-path as img.src
attachment = attachment_by_api_content_src(work_package, src)
attachment = attachment_by_api_content_src(src)
return nil if attachment.nil? || !pdf_embeddable?(attachment.content_type)
local_file = attachment_image_local_file(attachment)
return nil if local_file.nil?
resize_image(local_file.path)
filename = local_file.path
filename = convert_gif_to_png(filename) if attachment.content_type == "image/gif"
resize_image(filename)
end
def attachment_by_api_content_src(_work_package, src)
def temp_image_file(extension)
tmp_file = Tempfile.new(["temp_image", extension])
@resized_images = [] if @resized_images.nil?
@resized_images << tmp_file
tmp_file.path
end
def convert_gif_to_png(filename)
tmp_file = temp_image_file(".png")
image = MiniMagick::Image.open(filename)
image.frames.first.write(tmp_file)
tmp_file
end
def attachment_by_api_content_src(src)
attachment_regex = %r{/attachments/(\d+)/content}
return nil unless src&.match?(attachment_regex)
@@ -33,7 +33,7 @@ module WorkPackage::PDFExport::Common::Badge
def initialize(options)
@color = options[:color]
@document = options[:document]
@radius = options[:radius]
@radius = options[:radius] || 0
@offset = options[:offset] || 0
end
@@ -45,21 +45,30 @@ module WorkPackage::PDFExport::Common::Badge
end
end
def calc_grayscale_brightness(red, green, blue)
# The brightness is calculated using the formula:
# https://entropymine.com/imageworsener/grayscale/
Math.sqrt(
(0.2126 * (red**2)) +
(0.7152 * (green**2)) +
(0.0722 * (blue**2))
).to_i
end
def readable_color(pdf_background_color)
sum = [
brightness = calc_grayscale_brightness(*[
pdf_background_color[0..1],
pdf_background_color[2..3],
pdf_background_color[4..5]
].sum { |color_part| color_part.to_i(16) }
lightness = sum / 3
lightness < 130 ? "FFFFFF" : "000000"
].map { |color_part| color_part.to_i(16) })
brightness < 130 ? "FFFFFF" : "000000"
end
def prawn_badge(text, color, offset: 0)
badge = BadgeCallback.new({ color: color, radius: 8, document: pdf, offset: })
def prawn_badge(text, color, offset: 0, radius: 8, font_size: 8)
badge = BadgeCallback.new({ color: color, radius:, document: pdf, offset: })
{
text: (Prawn::Text::NBSP * 3) + text + (Prawn::Text::NBSP * 3),
size: 8,
size: font_size,
color: readable_color(color),
callback: badge
}
@@ -129,11 +129,15 @@ module WorkPackage::PDFExport::Common::Common
end
def draw_horizontal_line(top, left, right, height, color)
pdf.stroke do
previous_color = pdf.stroke_color
previous_line_width = pdf.line_width
@pdf.stroke do
pdf.stroke_color = color
pdf.line_width = height
pdf.horizontal_line left, right, at: top
end
pdf.stroke_color = previous_color
pdf.line_width = previous_line_width
end
def draw_styled_text(text, opts)
@@ -277,17 +281,17 @@ module WorkPackage::PDFExport::Common::Common
end
def footer_date
format_time(export_datetime)
format_date(export_datetime)
end
def current_page_nr
pdf.page_number + @page_count - (with_cover? ? 1 : 0)
end
def write_horizontal_line(y_position, height, color)
def write_horizontal_line(y_position, height, color, left_padding: 0)
draw_horizontal_line(
y_position,
pdf.bounds.left, pdf.bounds.right,
pdf.bounds.left + left_padding, pdf.bounds.right,
height, color
)
end
@@ -297,6 +301,30 @@ module WorkPackage::PDFExport::Common::Common
pdf.start_new_page unless is_first_on_page
end
# Prawn table does not support inline formatting other than inline HTML formatting, so we have to convert the styling
def prawn_table_cell_inline_formatting_data(text, style) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
value = text || ""
value = "<link href=\"#{style[:link]}\">#{value}</link>" if style.key?(:link)
value = "<link anchor=\"#{style[:anchor]}\">#{value}</link>" if style.key?(:anchor)
value = "<color rgb=\"#{style[:color]}\">#{value}</color>" if style.include?(:color)
value = "<font size=\"#{style[:size]}\">#{value}</font>" if style.key?(:size)
value = "<font character_spacing=\"#{style[:character_spacing]}\">#{value}</font>" if style.key?(:character_spacing)
value = "<font name=\"#{style[:font]}\">#{value}</font>" if style.key?(:font)
prawn_table_cell_inline_font_styles(value, style[:styles] || [])
end
# Prawn table does not support inline formatting other than inline HTML formatting, so we have to convert the styles
def prawn_table_cell_inline_font_styles(text, styles)
value = text || ""
value = "<b>#{value}</b>" if styles.include?(:bold)
value = "<i>#{value}</i>" if styles.include?(:italic)
value = "<u>#{value}</u>" if styles.include?(:underline)
value = "<strikethrough>#{value}</strikethrough>" if styles.include?(:strikethrough)
value = "<sub>#{value}</sub>" if styles.include?(:sub)
value = "<sup>#{value}</sup>" if styles.include?(:sup)
value
end
def write_optional_page_break
space_from_bottom = pdf.y - pdf.bounds.bottom
if space_from_bottom < styles.page_break_threshold
@@ -29,48 +29,104 @@
module WorkPackage::PDFExport::Common::Macro
PREFORMATTED_BLOCKS = %w(pre code).freeze
def apply_markdown_field_macros(markdown, work_package)
apply_macros(markdown, work_package, WorkPackage::Exports::Macros::Attributes)
def apply_markdown_field_macros(markdown, context)
return markdown if markdown.blank?
apply_macros(markdown, context)
end
def macros
[
WorkPackage::Exports::Macros::Links,
WorkPackage::Exports::Macros::Attributes
]
end
private
def apply_macros(markdown, work_package, formatter)
return markdown unless formatter.applicable?(markdown)
def apply_macros(markdown, context)
return markdown unless macros.any? { |macro| macro.applicable?(markdown) }
document = Markly.parse(markdown)
document.walk do |node|
if node.type == :html
node.string_content = apply_macro_html(node.string_content, work_package, formatter) || node.string_content
elsif node.type == :text
node.string_content = apply_macro_text(node.string_content, work_package, formatter) || node.string_content
document.walk { |node| apply_macros_node(node, context) }
document.to_markdown
.gsub("\\~", "~") # fix a bug in Markly that escapes tildes
end
def apply_macros_node(node, context)
if node.type == :html
apply_macros_node_html(node, context)
elsif node.type == :text && not_in_mention?(node)
apply_macros_node_text(node, context)
end
end
def not_in_mention?(node)
# Check if the text node is not inside a mention tag
# e.g. <mention ...>#1234</mention>
# We don't want to end up with
# <mention ...><mention ...>#1234</mention></mention>
node.previous.nil? || node.previous.type != :inline_html || node.previous.string_content.exclude?("<mention")
end
def apply_macros_node_text(node, context)
formatted, applied_macros = apply_macro_text(node.string_content, context)
return if formatted == node.string_content
if has_html_macro?(applied_macros)
fragment = Markly::Node.new(:inline_html)
fragment.string_content = formatted
node.insert_before(fragment)
node.delete
else
node.string_content = formatted
end
end
def apply_macros_node_html(node, context)
node.string_content = apply_macro_html(node.string_content, context)
end
def applicable?(content)
macros.any? { |macro| macro.applicable?(content) }
end
def apply_macro_text(text, context)
applicable_macros = macros.select { |macro| macro.applicable?(text) }
return text if applicable_macros.empty?
applicable_macros.each do |macro|
text = text.gsub(macro.regexp) do |matched_string|
macro.process_match(Regexp.last_match, matched_string, context)
end
end
document.to_markdown
[text, applicable_macros]
end
def apply_macro_text(text, work_package, formatter)
return text unless formatter.applicable?(text)
text.gsub!(formatter.regexp) do |matched_string|
matchdata = Regexp.last_match
formatter.process_match(matchdata, matched_string, { user: User.current, work_package: })
end
end
def apply_macro_html(html, work_package, formatter)
return html unless formatter.applicable?(html)
def apply_macro_html(html, context)
return html unless applicable?(html)
doc = Nokogiri::HTML.fragment(html)
apply_macro_html_node(doc, work_package, formatter)
apply_macro_html_node(doc, context)
doc.to_html
end
def apply_macro_html_node(node, work_package, formatter)
def has_html_macro?(macros)
macros.any? { |macro| macro.respond_to?(:html_replacement?) && macro.html_replacement? }
end
def apply_macro_html_node(node, context)
if node.text?
node.content = apply_macro_text(node.content, work_package, formatter)
formatted, applied_macros = apply_macro_text(node.content, context)
if formatted != node.content
if has_html_macro?(applied_macros)
node.replace(formatted)
else
node.content = formatted
end
end
elsif PREFORMATTED_BLOCKS.exclude?(node.name)
node.children.each { |child| apply_macro_html_node(child, work_package, formatter) }
node.children.each { |child| apply_macro_html_node(child, context) }
end
end
end
@@ -86,7 +86,7 @@ module WorkPackage::PDFExport::Common::StylesPage
end
def page_break_threshold
resolve_pt(@styles.dig(:page, :page_break_threshold), 200)
@page_break_threshold ||= resolve_pt(@styles.dig(:page, :page_break_threshold), 200)
end
def link_color
@@ -65,7 +65,8 @@ class WorkPackage::PDFExport::DocumentGenerator < Exports::Exporter
def render_doc
generate_doc!(
apply_markdown_field_macros(work_package.description || "", work_package),
apply_markdown_field_macros(work_package.description || "",
{ work_package:, project: work_package.project, user: User.current }),
"contracts.yml"
)
end
@@ -34,6 +34,7 @@ module WorkPackage::PDFExport::Export::Markdown
class MD2PDFExport
include MarkdownToPDF::Core
include MarkdownToPDF::Parser
include WorkPackage::PDFExport::Common::Common
def initialize(styling_yml, pdf, hyphenation_language)
@styles = MarkdownToPDF::Styles.new(styling_yml)
@@ -68,9 +69,8 @@ module WorkPackage::PDFExport::Export::Markdown
@hyphens.hyphenate(text)
end
def handle_mention_html_tag(tag, node, opts)
def handle_user_mention_html_tag(tag, node, opts)
if tag.text.blank?
# <mention class="mention" data-id="46012" data-type="work_package" data-text="#46012"></mention>
# <mention class="mention" data-id="3" data-type="user" data-text="@Some User">
text = tag.attr("data-text")
if text.present? && !node.next.respond_to?(:string_content) && node.next.string_content != text
@@ -78,9 +78,70 @@ module WorkPackage::PDFExport::Export::Markdown
end
end
# <mention class="mention" data-id="3" data-type="user" data-text="@Some User">@Some User</mention>
# the node text is used.
[]
end
def handle_wp_mention_html_tag(tag, node, opts)
# <mention class="mention" data-id="185" data-type="work_package" data-text="#185">#185</mention>
# <mention class="mention" data-id="185" data-type="work_package" data-text="##185">##185</mention>
# <mention class="mention" data-id="185" data-type="work_package" data-text="###185">###185</mention>
id = tag.attr("data-id")
return [] if id.blank?
next_node = node&.next # there is no markdown node in a html table
if next_node && next_node.type == :text && next_node.respond_to?(:string_content)
# clear the text content, so it does not get rendered
next_node.string_content = ""
end
wp_mention_macro(tag.attr("data-text") || "", id, opts)
end
def wp_mention_macro(content, id, opts)
count = content.count("#")
if count > 1
work_package = WorkPackage.find_by(id: id)
unless work_package.nil? || !work_package.visible?
# ##1234: {Type} #{ID}: {Subject}
content = "#{work_package.type} ##{work_package.id}: #{work_package.subject}"
if count == 3
# ###1234: {Status} {Type} #{ID}: {Subject} ({Start Date} - {End Date})
content = "#{work_package.status.name} #{content}#{work_package_dates(work_package)}"
end
end
end
[text_hash(content, opts.merge({ link: url_helpers.work_package_url(id) }))]
end
def work_package_dates(work_package)
return "" if work_package.start_date.blank? && work_package.due_date.blank?
if work_package.due_date.present? && work_package.start_date == work_package.due_date
return " (#{format_date(work_package.due_date)})"
end
work_package_date_range(work_package)
end
def work_package_date_range(work_package)
content = [
work_package.start_date.present? ? format_date(work_package.start_date) : I18n.t("label_no_start_date"),
work_package.due_date.present? ? format_date(work_package.due_date) : I18n.t("label_no_due_date")
].join(" - ")
" (#{content})"
end
def handle_mention_html_tag(tag, node, opts)
type = tag.attr("data-type")
if type == "work_package"
handle_wp_mention_html_tag(tag, node, opts)
elsif type == "user"
handle_user_mention_html_tag(tag, node, opts)
else
[]
end
end
def handle_unknown_inline_html_tag(tag, node, opts)
result = if tag.name == "mention"
handle_mention_html_tag(tag, node, opts)
@@ -93,9 +154,13 @@ module WorkPackage::PDFExport::Export::Markdown
end
def handle_unknown_html_tag(_tag, _node, opts)
# unknown/unsupported html tags eg. <foo>hi</foo> are ignored
# but scanned for supported or text children [true, ...]
[true, opts]
if tag.name == "mention"
handle_mention_html_tag(tag, node, opts)
else
# unknown/unsupported html tags eg. <foo>hi</foo> are ignored
# but scanned for supported or text children [true, ...]
[true, opts]
end
end
def warn(text, element, node)
@@ -103,10 +168,14 @@ module WorkPackage::PDFExport::Export::Markdown
end
end
def write_markdown!(work_package, markdown, styling_yml)
md2pdf = MD2PDFExport.new(styling_yml, pdf, hyphenation_language)
md2pdf.draw_markdown(markdown, pdf, ->(src) {
with_images? ? attachment_image_filepath(work_package, src) : nil
})
def markdown_writer(styling_yml)
@markdown_writer ||= MD2PDFExport.new(styling_yml, pdf, hyphenation_language)
end
def write_markdown!(markdown, styling_yml)
markdown_writer(styling_yml)
.draw_markdown(markdown, pdf, ->(src) {
with_images? ? attachment_image_filepath(src) : nil
})
end
end
@@ -51,8 +51,8 @@ module WorkPackage::PDFExport::Export::MarkdownField
def write_markdown_field_value(work_package, markdown)
with_margin(styles.wp_markdown_margins) do
write_markdown!(
work_package,
apply_markdown_field_macros(markdown, work_package),
apply_markdown_field_macros(markdown,
{ work_package:, project: work_package.project, user: User.current }),
styles.wp_markdown_styling_yml
)
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,459 @@
page:
page_size: "A4"
margin_top: 60
margin_bottom: 60
margin_left: 36
margin_right: 36
page_break_threshold: 100
link_color: "175A8E"
page_header:
offset: 20
size: 8
page_footer:
offset: -30
size: 8
spacing: 6
page_logo:
height: 20
align: "right"
page_heading:
size: 14
styles: [ "bold" ]
margin_bottom: 2
page_subtitle:
margin_top: 4
margin_bottom: 10
size: 10
cover:
header:
logo_height: 25
border:
color: 'd3dee3'
height: 1
offset: 6
footer:
size: 10
color: '414d5f'
offset: 30
hero:
padding_right: 144
padding_top: 120
title:
max_height: 30
spacing: 10
font: 'SpaceMono'
color: '414d5f'
size: 10
heading:
spacing: 14
color: '414d5f'
styles:
- bold
size: 16
dates:
spacing: 4
max_height: 16
color: '414d5f'
size: 10
styles:
- bold
subheading:
max_height: 30
color: '414d5f'
size: 10
notes:
markdown_margin:
margin: 16
markdown:
font:
size: 10
leading: 3
header:
size: 8
styles: [ "bold" ]
padding_top: 4
header_1:
size: 10
header_2:
size: 10
header_3:
size: 9
paragraph:
align: "left"
unordered_list:
spacing: 1
unordered_list_point:
spacing: 4
ordered_list:
spacing: 1
ordered_list_point:
spacing: 4
spanning: true
list_style_type: decimal
ordered_list_point_2:
list_style_type: lower-latin
ordered_list_point_3:
list_style_type: lower-roman
ordered_list_point_4:
list_style_type: upper-latin
ordered_list_point_5:
list_style_type: upper-roman
task_list:
spacing: 1
task_list_point:
spacing: 4
checked: "☑"
unchecked: "☐"
link:
color: "175A8E"
styles: [ ]
code:
color: "880000"
size: 9
font: "SpaceMono"
blockquote:
background_color: "f4f9ff"
size: 10
styles: [ "italic" ]
color: "0f3b66"
border_color: "b8d6f4"
border_width: 1
padding: 4
padding_left: 6
margin_top: 4
margin_bottom: 4
no_border_left: false
no_border_right: true
no_border_bottom: true
no_border_top: true
image:
align: "center"
margin_bottom: 4
caption:
size: 8
align: "center"
codeblock:
background_color: "F5F5F5"
color: "880000"
padding: 10
size: 8
margin_top: 10
margin_bottom: 10
font: "SpaceMono"
table:
auto_width: true
margin_top: 4
margin_bottom: 4
header:
size: 9
styles: [ "bold" ]
background_color: "F0F0F0"
cell:
size: 8
border_width: 0.25
padding: 5
html_table:
auto_width: true
margin_top: 4
margin_bottom: 4
header:
size: 9
styles: [ "bold" ]
cell:
size: 8
border_width: 0.25
padding: 5
alerts:
NOTE:
border_color: '0969da'
alert_color: '0969da'
padding: '4mm'
size: 10
styles: [ ]
border_width: 2
no_border_right: true
no_border_left: false
no_border_bottom: true
no_border_top: true
TIP:
border_color: '1a7f37'
alert_color: '1a7f37'
padding: '4mm'
size: 10
styles: [ ]
border_width: 2
no_border_right: true
no_border_left: false
no_border_bottom: true
no_border_top: true
IMPORTANT:
border_color: '8250df'
alert_color: '8250df'
padding: '4mm'
size: 10
styles: [ ]
border_width: 2
no_border_right: true
no_border_left: false
no_border_bottom: true
no_border_top: true
WARNING:
border_color: 'bf8700'
alert_color: 'bf8700'
padding: '4mm'
size: 10
styles: [ ]
border_width: 2
no_border_right: true
no_border_left: false
no_border_bottom: true
no_border_top: true
CAUTION:
border_color: 'd1242f'
alert_color: 'd1242f'
size: 10
styles: [ ]
padding: '4mm'
border_width: 2
no_border_right: true
no_border_left: false
no_border_bottom: true
no_border_top: true
outcome:
indent: 15
title:
size: 10
styles: [ "bold" ]
margin_top: 5
margin_bottom: 5
markdown_margin:
margin: 16
markdown:
font:
size: 10
leading: 3
header:
size: 8
styles: [ "bold" ]
padding_top: 4
header_1:
size: 10
header_2:
size: 10
header_3:
size: 9
paragraph:
align: "left"
unordered_list:
spacing: 1
unordered_list_point:
spacing: 4
ordered_list:
spacing: 1
ordered_list_point:
spacing: 4
spanning: true
list_style_type: decimal
ordered_list_point_2:
list_style_type: lower-latin
ordered_list_point_3:
list_style_type: lower-roman
ordered_list_point_4:
list_style_type: upper-latin
ordered_list_point_5:
list_style_type: upper-roman
task_list:
spacing: 1
task_list_point:
spacing: 4
checked: "☑"
unchecked: "☐"
link:
color: "175A8E"
styles: [ ]
code:
color: "880000"
size: 9
font: "SpaceMono"
blockquote:
background_color: "f4f9ff"
size: 10
styles: [ "italic" ]
color: "0f3b66"
border_color: "b8d6f4"
border_width: 1
padding: 4
padding_left: 6
margin_top: 4
margin_bottom: 4
no_border_left: false
no_border_right: true
no_border_bottom: true
no_border_top: true
image:
align: "center"
margin_bottom: 4
caption:
size: 8
align: "center"
codeblock:
background_color: "F5F5F5"
color: "880000"
padding: 10
size: 8
margin_top: 10
margin_bottom: 10
font: "SpaceMono"
table:
auto_width: true
margin_top: 4
margin_bottom: 4
header:
size: 9
styles: [ "bold" ]
background_color: "F0F0F0"
cell:
size: 8
border_width: 0.25
padding: 5
html_table:
auto_width: true
margin_top: 4
margin_bottom: 4
header:
size: 9
styles: [ "bold" ]
cell:
size: 8
border_width: 0.25
padding: 5
alerts:
NOTE:
border_color: '0969da'
alert_color: '0969da'
padding: '4mm'
size: 10
styles: [ ]
border_width: 2
no_border_right: true
no_border_left: false
no_border_bottom: true
no_border_top: true
TIP:
border_color: '1a7f37'
alert_color: '1a7f37'
padding: '4mm'
size: 10
styles: [ ]
border_width: 2
no_border_right: true
no_border_left: false
no_border_bottom: true
no_border_top: true
IMPORTANT:
border_color: '8250df'
alert_color: '8250df'
padding: '4mm'
size: 10
styles: [ ]
border_width: 2
no_border_right: true
no_border_left: false
no_border_bottom: true
no_border_top: true
WARNING:
border_color: 'bf8700'
alert_color: 'bf8700'
padding: '4mm'
size: 10
styles: [ ]
border_width: 2
no_border_right: true
no_border_left: false
no_border_bottom: true
no_border_top: true
CAUTION:
border_color: 'd1242f'
alert_color: 'd1242f'
size: 10
styles: [ ]
padding: '4mm'
border_width: 2
no_border_right: true
no_border_left: false
no_border_bottom: true
no_border_top: true
heading:
size: 12
styles: [ "bold" ]
margin_bottom: 8
hr:
color: "6E7781"
height: 1.5
margin_bottom: 10
participants:
margin_bottom: 8
cell:
size: 10
padding_left: 0
padding_bottom: 7
no_border: true
attachments:
margin_bottom: 8
cell:
size: 10
padding_left: 0
padding_bottom: 7
no_border: true
agenda_section:
title:
size: 12
color: "000000"
styles: [ "bold" ]
subtitle:
size: 11
color: "636C76"
title_cell:
padding_top: 2
padding_bottom: 10
padding_left: 5
padding_right: 5
no_border: true
background_color: "EAEAEA"
title_margins:
margin_top: 15
margin_bottom: 5
agenda_item:
indent: 5
title_margin:
margin_bottom: 10
title_cell:
padding_top: 0
padding_bottom: 0
padding_left: 5
padding_right: 5
no_border: true
title:
styles: [ "bold" ]
size: 11
subtitle:
color: "636C76"
size: 10
hr:
color: "D0D7DE"
height: 1
margin_top: 6
margin_bottom: 6
@@ -0,0 +1,163 @@
# 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.
#++
module WorkPackage::PDFExport::Export::Meetings::Styles
class PDFStyles
include MarkdownToPDF::Common
include MarkdownToPDF::StyleHelper
include WorkPackage::PDFExport::Common::Styles
include WorkPackage::PDFExport::Common::StylesPage
include WorkPackage::PDFExport::Common::StylesCover
def page_subtitle
resolve_font(@styles[:page_subtitle])
end
def page_subtitle_margins
resolve_margin(@styles[:page_subtitle])
end
def notes_markdown_margins
resolve_margin(@styles.dig(:work_package, :markdown_margin))
end
def notes_markdown_styling_yml
resolve_markdown_styling(@styles.dig(:notes, :markdown) || {})
end
def outcome_markdown_styling_yml
resolve_markdown_styling(@styles.dig(:outcome, :markdown) || {})
end
def heading
resolve_font(@styles[:heading])
end
def heading_hr
{
color: @styles.dig(:heading, :hr, :color),
height: resolve_pt(@styles.dig(:heading, :hr, :height), 1)
}
end
def heading_hr_margins
resolve_margin(@styles.dig(:heading, :hr))
end
def agenda_item_title_margins
resolve_margin(@styles.dig(:agenda_item, :title_margin))
end
def agenda_item_indent
@styles.dig(:agenda_item, :indent).presence || 5
end
def outcome_title
resolve_font(@styles.dig(:outcome, :title))
end
def outcome_title_margins
resolve_margin(@styles.dig(:outcome, :title))
end
def outcome_indent
@styles.dig(:outcome, :indent).presence || 15
end
def agenda_item_title
resolve_font(@styles.dig(:agenda_item, :title))
end
def agenda_item_title_cell
resolve_table_cell(@styles.dig(:agenda_item, :title_cell))
end
def agenda_item_subtitle
resolve_font(@styles.dig(:agenda_item, :subtitle))
end
def agenda_item_hr
{
color: @styles.dig(:agenda_item, :hr, :color),
height: resolve_pt(@styles.dig(:agenda_item, :hr, :height), 1)
}
end
def agenda_item_margins
resolve_margin(@styles.dig(:agenda_item, :hr))
end
def heading_margins
resolve_margin(@styles[:heading])
end
def participants_table_cell
resolve_table_cell(@styles.dig(:participants, :cell))
end
def participants_margins
resolve_margin(@styles[:participants])
end
def attachments_table_cell
resolve_table_cell(@styles.dig(:attachments, :cell))
end
def attachments_margins
resolve_margin(@styles[:attachments])
end
def agenda_section_title
resolve_font(@styles.dig(:agenda_section, :title))
end
def agenda_section_title_table_margins
resolve_margin(@styles.dig(:agenda_section, :title_margins))
end
def agenda_section_subtitle
resolve_font(@styles.dig(:agenda_section, :subtitle))
end
def agenda_section_title_cell
resolve_table_cell(@styles.dig(:agenda_section, :title_cell))
end
end
def styles
@styles ||= PDFStyles.new(styles_asset_path)
end
private
def styles_asset_path
File.dirname(File.expand_path(__FILE__))
end
end
@@ -135,7 +135,7 @@ module WorkPackage::PDFExport::Generator::Generator
if src == logo_image_filename
logo_image_filename
else
attachment_image_filepath(work_package, src)
attachment_image_filepath(src)
end
})
end
@@ -26,7 +26,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module WorkPackages::Scopes::CoveringDatesAndDaysOfWeek
module WorkPackages::Scopes::CoveringDatesOrDaysOfWeek
extend ActiveSupport::Concern
using CoreExtensions::SquishSql
@@ -38,7 +38,7 @@ module WorkPackages::Scopes::CoveringDatesAndDaysOfWeek
# @param dates Date[] An array of the Date objects.
# @param days_of_week number[] An array of the ISO days of the week to
# consider. 1 is Monday, 7 is Sunday.
def covering_dates_and_days_of_week(days_of_week: [], dates: [])
def covering_dates_or_days_of_week(days_of_week: [], dates: [])
work_packages_periods_cte = work_packages_periods_cte_for_covering_work_packages
where_covers_periods(work_packages_periods_cte, days_of_week, dates)
end
@@ -59,30 +59,28 @@ module WorkPackages::Scopes::CoveringDatesAndDaysOfWeek
-- select work packages dates
WITH
-- cte returning a table with work package id, period start_date and end_date
#{work_packages_periods_cte},
-- All days between the start date of a work package and its due date
covered_dates AS (
SELECT
id,
generate_series(work_packages_periods.start_date,
work_packages_periods.end_date,
'1 day') AS date
FROM work_packages_periods
),
-- All days between the start date of a work package and its due date including the day of the week for each date
covered_dates_and_wday AS (
SELECT
id,
date,
EXTRACT(isodow FROM date) dow
FROM covered_dates
)
#{work_packages_periods_cte}
-- select id of work packages covering the given days
SELECT id FROM covered_dates_and_wday
WHERE dow IN (:days_of_week) OR date IN (:dates)
SELECT id
FROM work_packages_periods
WHERE
-- Check if the range covers any of the provided days of week. It is
-- done by comparing number of days from start_date to first target
-- day of week with number of days in range. If number is less or
-- eqal, then the range covers the target day of week
EXISTS (
SELECT 1
FROM UNNEST(ARRAY[:days_of_week]::INT[]) AS target_dow
WHERE (target_dow + 7 - EXTRACT(ISODOW FROM start_date)::INT) % 7 <= (end_date - start_date)
)
OR
-- Check if the range covers any of the provided dates
EXISTS (
SELECT 1
FROM unnest(Array[:dates]::DATE[]) AS target_date
WHERE target_date BETWEEN start_date AND end_date
)
SQL
covering_work_packages_query_sql = sanitize_sql([covering_work_packages_query_sql, { days_of_week:, dates: }])
@@ -146,8 +144,8 @@ module WorkPackages::Scopes::CoveringDatesAndDaysOfWeek
work_packages_periods AS (
SELECT DISTINCT ON (succ_id)
pred_id as id,
pred_date as start_date,
succ_date as end_date
pred_date::DATE as start_date,
succ_date::DATE as end_date
FROM automatic_follows_relations
ORDER BY succ_id, pred_date ASC
)
@@ -29,6 +29,8 @@ module BasicData
class BaseRoleSeeder < ModelSeeder
self.needs = []
self.attribute_names_for_lookups = %i[name]
def model_attributes(role_data)
{
type:,
+1
View File
@@ -33,6 +33,7 @@ module BasicData
BasicData::ColorSeeder,
BasicData::ColorSchemeSeeder
]
self.attribute_names_for_lookups = %i[name]
def model_attributes(type_data)
{
+4 -22
View File
@@ -26,40 +26,22 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module BasicData
class WorkflowSeeder < Seeder
class WorkflowSeeder < ModelSeeder
self.needs = [
BasicData::ProjectRoleSeeder,
BasicData::GlobalRoleSeeder,
BasicData::StatusSeeder,
BasicData::TypeSeeder
]
self.model_class = Workflow
self.seed_data_model_key = "workflows"
def seed_data!
if any_types_or_statuses_or_workflows_already_configured?
print_status " *** Skipping types, statuses and workflows as there are already some configured"
else
seed_statuses
seed_types
seed_workflows
end
seed_workflows
end
private
def any_types_or_statuses_or_workflows_already_configured?
Type.where(is_standard: false).any? || Status.any? || Workflow.any?
end
def seed_statuses
print_status " ↳ Statuses"
BasicData::StatusSeeder.new(seed_data).seed!
end
def seed_types
print_status " ↳ Types"
BasicData::TypeSeeder.new(seed_data).seed!
end
def seed_workflows
member = seed_data.find_reference(:default_role_member)
project_admin = seed_data.find_reference(:default_role_project_admin)
+10 -1
View File
@@ -60,6 +60,11 @@ module DemoData
]
end
def applicable?
seed_data.reference_exists?(:default_role_project_admin) &&
types_seed_data.all? { |type_reference| seed_data.reference_exists?(type_reference) }
end
private
attr_writer :project
@@ -94,7 +99,7 @@ module DemoData
def set_types
print_status " -Assigning types."
project.types = seed_data.find_references(project_data.lookup("types"))
project.types = seed_data.find_references(types_seed_data)
end
def seed_categories
@@ -142,6 +147,10 @@ module DemoData
end
end
def types_seed_data
seed_data.lookup("types") || []
end
def project_attributes
parent = Project.find_by(identifier: project_data.lookup("parent"))
{
+10 -8
View File
@@ -34,8 +34,8 @@ module DemoData
print_status " ↳ Updating settings"
seed_settings
seed_data.each_data("projects") do |project_data|
seed_project(project_data)
project_seeders.each do |project_seeder|
project_seeder.seed!
Setting.demo_projects_available = true
end
@@ -44,7 +44,7 @@ module DemoData
end
def applicable?
Project.count.zero?
Project.count.zero? && project_seeders.all?(&:applicable?)
end
def seed_settings
@@ -55,11 +55,6 @@ module DemoData
end
end
def seed_project(project_data)
project_seeder = ProjectSeeder.new(project_data)
project_seeder.seed!
end
def seed_form_configuration
BasicData::TypeConfigurationSeeder.new(seed_data).seed!
end
@@ -74,5 +69,12 @@ module DemoData
welcome_on_homescreen: 1
}
end
private
def project_seeders
@project_seeders ||= seed_data.each_data("projects")
.map { |project_data| ProjectSeeder.new(project_data) }
end
end
end
@@ -47,7 +47,9 @@ module DevelopmentData
end
def applicable?
recent_installation? && Project.where(identifier: project_identifiers).count == 0
recent_installation? &&
Project.where(identifier: project_identifiers).count == 0 &&
seed_data.reference_exists?(:default_role_project_admin)
end
# returns true if no projects have been created more than 1 hour ago,
+4
View File
@@ -37,6 +37,10 @@ class DevelopmentDataSeeder < CompositeSeeder
]
end
def applicable?
DevelopmentData::CustomFieldsSeeder.new.applicable?
end
def namespace
"DevelopmentData"
end
+7 -3
View File
@@ -36,7 +36,7 @@ class Source::SeedData
def store_reference(reference, record)
return if reference.nil?
if registry.key?(reference)
if reference_exists?(reference)
raise ArgumentError, "an object with reference #{reference.inspect} is already registered"
end
@@ -53,7 +53,7 @@ class Source::SeedData
def find_reference(reference, *fallbacks, default: :__unset__)
return if reference.nil?
existing_ref = [reference, *fallbacks].find { |ref| registry.key?(ref) }
existing_ref = [reference, *fallbacks].find { |ref| reference_exists?(ref) }
if existing_ref
registry[existing_ref]
elsif default != :__unset__
@@ -72,6 +72,10 @@ class Source::SeedData
Array(references).map { |reference| find_reference(reference, default:) }
end
def reference_exists?(reference)
registry.key?(reference)
end
# Get a `SeedData` instance with only the given top level keys.
#
# Used in tests to get the real statuses, types and other data.
@@ -110,7 +114,7 @@ class Source::SeedData
def each_data(path)
sub_data = fetch(path)
return if sub_data.nil?
return to_enum(:each_data, path) unless block_given? || sub_data.nil?
sub_data.each_value do |item_data|
yield self.class.new(item_data, registry)
@@ -39,6 +39,8 @@ module Standard
::BasicData::ColorSchemeSeeder,
::BasicData::ProjectPhaseColorSeeder,
::BasicData::ProjectPhaseDefinitionSeeder,
::BasicData::StatusSeeder,
::BasicData::TypeSeeder,
::BasicData::WorkflowSeeder,
::BasicData::PrioritySeeder,
::BasicData::SettingSeeder,
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module ProjectLifeCycleSteps
module ProjectPhases
class ActivationService < ::BaseServices::BaseContracted
alias_method :project, :model
@@ -88,7 +88,7 @@ module ProjectLifeCycleSteps
end
def default_contract_class
ProjectLifeCycleSteps::ActivationContract
ProjectPhases::ActivationContract
end
end
end
@@ -28,24 +28,26 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module ProjectLifeCycleSteps
class UpdateService < ::BaseServices::Update
delegate :project, to: :model
module ProjectPhases
class RescheduleService < ::BaseServices::BaseContracted
alias_method :project, :model
def after_perform(*)
reschedule_following_phases if model.date_range_set?
project.touch_and_save_journals
super
def initialize(user:, project:, contract_class: nil, contract_options: {})
super(user:, contract_class:, contract_options:)
self.model = project
end
private
def reschedule_following_phases
from = initial_reschedule_date
def persist(service_call)
reschedule_phases(**params)
following_phases.each do |phase|
service_call
end
def reschedule_phases(phases:, from:)
phases.each do |phase|
next unless phase.active?
next unless phase.date_range_set?
next unless phase.duration&.positive? # not updated using service that sets duration
@@ -58,14 +60,6 @@ module ProjectLifeCycleSteps
end
end
def initial_reschedule_date
model.active? ? model.finish_date + 1 : model.start_date
end
def following_phases
project.available_phases.select { it.position > model.position }
end
def calculate_date_range(from, duration:)
days = working_days_from(from, count: duration)
@@ -80,5 +74,9 @@ module ProjectLifeCycleSteps
end
days
end
def default_contract_class
ProjectPhases::RescheduleContract
end
end
end
@@ -26,7 +26,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module ProjectLifeCycleSteps
module ProjectPhases
class SetAttributesService < ::BaseServices::SetAttributes
def perform(*)
super.tap do
@@ -0,0 +1,58 @@
# 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.
#++
module ProjectPhases
class UpdateService < ::BaseServices::Update
delegate :project, to: :model
def after_perform(*)
reschedule_following_phases if model.date_range_set?
project.touch_and_save_journals
super
end
private
def reschedule_following_phases
RescheduleService.new(user:, project:)
.call(phases: following_phases, from: initial_reschedule_date)
end
def initial_reschedule_date
model.active? ? model.finish_date + 1 : model.start_date
end
def following_phases
project.available_phases.select { it.position > model.position }
end
end
end
@@ -28,29 +28,27 @@
# See COPYRIGHT and LICENSE files for more details.
# ++
module My
module TimeTracking
class ViewModeSwitcherComponent < ApplicationComponent
options :mode,
:view_mode,
:date
def call
render(Primer::Alpha::TabNav.new(label: I18n.t(:label_view_mode_switcher))) do |component|
component.with_tab(selected: view_mode == :calendar, href: link(:calendar)) do |tab|
tab.with_text { I18n.t(:label_calendar) }
tab.with_icon(icon: :calendar)
end
component.with_tab(selected: view_mode == :list, href: link(:list)) do |tab|
tab.with_text { I18n.t(:label_list) }
tab.with_icon(icon: "op-view-list")
end
end
module Projects
module Copy
class PhasesDependentService < Dependency
def self.human_name
I18n.t("label_life_cycle_step_plural")
end
def link(new_view_mode)
my_time_tracking_path(date: date, mode: mode, view_mode: new_view_mode)
def source_count
source.phases.count
end
def copy_dependency(_params)
source.phases.each do |source_phase|
target.phases.create! **source_phase.attributes.slice(
"definition_id",
"start_date",
"finish_date",
"active",
"duration"
)
end
end
end
end
+8 -1
View File
@@ -33,7 +33,7 @@ module Projects
include Projects::Concerns::NewProjectService
def self.copy_dependencies
[
dependencies = [
::Projects::Copy::MembersDependentService,
::Projects::Copy::VersionsDependentService,
::Projects::Copy::CategoriesDependentService,
@@ -43,8 +43,15 @@ module Projects
::Projects::Copy::QueriesDependentService,
::Projects::Copy::BoardsDependentService,
::Projects::Copy::OverviewDependentService,
::Projects::Copy::PhasesDependentService,
::Projects::Copy::StoragesDependentService
]
if OpenProject::FeatureDecisions.stages_and_gates_active?
dependencies
else
dependencies - [::Projects::Copy::PhasesDependentService]
end
end
# Project Folders and File Links aren't dependent services anymore,
@@ -58,11 +58,16 @@ class Settings::WorkingDaysAndHoursUpdateService < Settings::UpdateService
def after_perform(call)
super.tap do
WorkPackages::ApplyWorkingDaysChangeJob.perform_later(
user_id: User.current.id,
previous_working_days:,
previous_non_working_days:
)
[
Projects::Phases::ApplyWorkingDaysChangeJob,
WorkPackages::ApplyWorkingDaysChangeJob
].each do |job_class|
job_class.perform_later(
user_id: User.current.id,
previous_working_days:,
previous_non_working_days:
)
end
end
end
@@ -31,4 +31,14 @@ See COPYRIGHT and LICENSE files for more details.
<%= render(Admin::CustomFields::EditFormHeaderComponent.new(custom_field: @custom_field, selected: :items)) %>
<%= render(Admin::CustomFields::Hierarchy::ItemsComponent.new(item: @active_item)) %>
<%=
render(Primer::Alpha::Layout.new(stacking_breakpoint: :md)) do |content|
content.with_main do
render(Admin::CustomFields::Hierarchy::ItemsComponent.new(item: @active_item))
end
content.with_sidebar(row_placement: :start, col_placement: :start, border: true, border_radius: 2, p: 3) do
render(Admin::CustomFields::Hierarchy::TreeViewComponent.new(custom_field: @custom_field, active_item: @active_item))
end
end
%>
@@ -26,6 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<% html_title t(:label_administration), t(:"attribute_help_texts.label_plural") %>
<% tabs = [
{

Some files were not shown because too many files have changed in this diff Show More