Merge branch 'dev' into task/48851-follow-up-with-passion-projects-what-needs-to-be-addedupdated-in-our-docs

# Conflicts:
#	docs/getting-started/my-account/README.md
#	docs/system-admin-guide/openproject_system_administration_start_page.png
This commit is contained in:
as-op
2023-08-07 13:51:43 +02:00
2542 changed files with 96247 additions and 66831 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
https://github.com/heroku/heroku-buildpack-nodejs.git#v197
https://github.com/pkgr/heroku-buildpack-ruby.git#v242-1
https://github.com/pkgr/heroku-buildpack-ruby.git#v254-2
+9 -8
View File
@@ -1,6 +1,7 @@
.github/
.git
.dockerignore
.gitignore
.bundle
.env*
.buildpacks
@@ -9,24 +10,24 @@
.*ignore
*.md
*.log
Vagrantfile
Dockerfile
docker/prod/Dockerfile
docker/ci/Dockerfile
Guardfile
docker-compose.*
browserslist
docs
!docs/api/apiv3/openapi-spec.yml
!docs/api/apiv3/paths
!docs/api/apiv3/tags
!docs/api/apiv3/components
/docs
!/docs/api/apiv3/openapi-spec.yml
!/docs/api/apiv3/paths
!/docs/api/apiv3/tags
!/docs/api/apiv3/components
extra
features
help
log/*.log
spec
tmp
/tmp
frontend/.angular/cache
frontend/node_modules
node_modules
+26 -12
View File
@@ -22,14 +22,18 @@ jobs:
strategy:
matrix:
include:
- base_prefix:
platform: linux/amd64
- base_prefix: ppc64le/
platform: linux/ppc64le
- platform: linux/amd64
target: slim
- platform: linux/arm64/v8
target: slim
- platform: linux/amd64
target: all-in-one
- platform: linux/ppc64le
bim_support: false
- base_prefix: arm64v8/
platform: linux/arm64/v8
target: all-in-one
- platform: linux/arm64/v8
bim_support: false
target: all-in-one
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -57,8 +61,8 @@ jobs:
with:
context: .
platforms: ${{ matrix.platform }}
target: ${{ matrix.target }}
build-args: |
BASE_PREFIX=${{ matrix.base_prefix }}
BIM_SUPPORT=${{ matrix.bim_support }}
pull: true
load: true
@@ -67,7 +71,7 @@ jobs:
- name: Test
# We only test the native container. If that fails the builds for the others
# will be cancelled as well.
if: matrix.platform == 'linux/amd64'
if: matrix.platform == 'linux/amd64' && matrix.target == 'all-in-one'
run: |
docker run \
--name openproject \
@@ -86,8 +90,8 @@ jobs:
with:
context: .
platforms: ${{ matrix.platform }}
target: ${{ matrix.target }}
build-args: |
BASE_PREFIX=${{ matrix.base_prefix }}
BIM_SUPPORT=${{ matrix.bim_support }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
@@ -99,20 +103,29 @@ jobs:
- name: Upload digest
uses: actions/upload-artifact@v3
with:
name: digests
name: digests-${{ matrix.target }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
strategy:
matrix:
target: [slim, all-in-one]
needs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v3
with:
name: digests
name: digests-${{ matrix.target }}
path: /tmp/digests
- name: Set suffix
id: set_suffix
run: |
suffix="-${{ matrix.target }}"
if [ "$suffix" = "-all-in-one" ]; then suffix="" ; fi
echo "suffix=$suffix" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Docker meta
@@ -122,11 +135,12 @@ jobs:
images: ${{ env.REGISTRY_IMAGE }}
flavor: |
latest=false
suffix=${{ steps.set_suffix.outputs.suffix }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=dev,enable={{is_default_branch}}
type=raw,value=dev,priority=200,enable={{is_default_branch}}
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
+7 -3
View File
@@ -1,5 +1,7 @@
name: pullpreview
concurrency: ${{ github.ref }}
on:
schedule:
# this is used to make sure no dangling resources are left
@@ -25,11 +27,13 @@ jobs:
- uses: actions/checkout@v3
- name: Generate .env.pullpreview file
run: |
echo "OP_ADMIN_USER_SEEDER_FORCE_PASSWORD_CHANGE=off" >> .env.pullpreview
echo "OPENPROJECT_SEED_ADMIN_USER_PASSWORD_RESET=false" >> .env.pullpreview
echo "OPENPROJECT_SHOW__SETTING__MISMATCH__WARNING=false" >> .env.pullpreview
echo "OPENPROJECT_FEATURE__STORAGES__MODULE__ACTIVE=true" >> .env.pullpreview
echo "OPENPROJECT_FEATURE__SHOW__CHANGES__ACTIVE=true" >> .env.pullpreview
echo "OPENPROJECT_LOOKBOOK__ENABLED=true" >> .env.pullpreview
echo "OPENPROJECT_HSTS=false" >> .env.pullpreview
echo "OPENPROJECT_ANGULAR_UGLIFY=false" >> .env.pullpreview
- name: Boot as BIM edition
if: contains(github.ref, 'bim/') || contains(github.head_ref, 'bim/')
run: |
@@ -42,11 +46,11 @@ jobs:
- uses: pullpreview/action@v5
with:
admins: crohr,HDinger,machisuji,oliverguenther,ulferts,wielinde,b12f,cbliard
always_on: dev
compose_files: docker-compose.pullpreview.yml
instance_type: large_2_0
instance_type: large
ports: 80,443,8080
default_port: 443
ttl: 10d
env:
AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.AWS_SECRET_ACCESS_KEY }}"
+1 -1
View File
@@ -64,7 +64,7 @@ npm-debug.log*
# Generated files
/app/assets/javascripts/editor/*
/app/assets/javascripts/locales/*.*
/frontend/src/locales/*.js
/frontend/src/locales/*
/modules/*/frontend/module/module
/config/additional_environment.rb
/config/configuration.yml
+7
View File
@@ -17,6 +17,8 @@ targets:
- imagemagick
debian-11:
<<: *debian
debian-12:
<<: *debian
ubuntu-20.04:
<<: *debian
ubuntu-22.04:
@@ -36,6 +38,11 @@ targets:
env:
- NODE_ENV=production
- NPM_CONFIG_PRODUCTION=false
centos-9:
<<: *centos7
env:
- NODE_ENV=production
- NPM_CONFIG_PRODUCTION=false
sles-12:
build_dependencies:
- sqlite3-devel
+1
View File
@@ -6,6 +6,7 @@ tmp/oas-generated.yml:
- '#/paths/~1api~1v3~1file_links~1{file_link_id}~1open/get/responses'
operation-4xx-response:
- '#/paths/~1api~1v3/get/responses'
- '#/paths/~1api~1v3~1help_texts/get/responses'
no-ambiguous-paths:
- '#/paths/~1api~1v3~1queries~1{id}~1form'
- '#/paths/~1api~1v3~1queries~1{id}~1star'
+1
View File
@@ -241,6 +241,7 @@ RSpec/ContextWording:
- 'on'
- to
- unless
- via
- when
- with
- within
+15 -10
View File
@@ -91,7 +91,7 @@ gem 'escape_utils', '~> 1.3'
# Syntax highlighting used in html-pipeline with rouge
gem 'rouge', '~> 4.1.0'
# HTML sanitization used for html-pipeline
gem 'sanitize', '~> 6.0.1'
gem 'sanitize', '~> 6.0.2'
# HTML autolinking for mails and urls (replaces autolink)
gem 'rinku', '~> 2.0.4'
# Version parsing with semver
@@ -114,9 +114,6 @@ gem 'mail', '= 2.8.1'
# provide compatible filesystem information for available storage
gem 'sys-filesystem', '~> 1.4.0', require: false
# Faster posix-compliant spawns for 8.0. conversions with pandoc
gem 'posix-spawn', '~> 0.3.13', require: false
gem 'bcrypt', '~> 3.1.6'
gem 'multi_json', '~> 1.15.0'
@@ -156,7 +153,7 @@ gem 'structured_warnings', '~> 0.4.0'
gem 'airbrake', '~> 13.0.0', require: false
gem 'prawn', '~> 2.2'
gem 'md_to_pdf', git: 'https://github.com/opf/md-to-pdf', tag: 'v0.0.18'
gem 'md_to_pdf', git: 'https://github.com/opf/md-to-pdf', ref: '76945d45c14b841e2312f992422e2631a4524114'
# prawn implicitly depends on matrix gem no longer in ruby core with 3.1
gem 'matrix', '~> 0.4.2'
@@ -170,7 +167,7 @@ group :production do
gem 'dalli', '~> 3.2.0'
end
gem 'i18n-js', '~> 3.9.0'
gem 'i18n-js', '~> 4.2.3'
gem 'rails-i18n', '~> 7.0.0'
gem 'sprockets', '~> 3.7.2' # lock sprockets below 4.0
@@ -202,10 +199,15 @@ gem 'mini_magick', '~> 4.12.0', require: false
gem 'validate_url'
# ActiveRecord extension which adds typecasting to store accessors
gem "store_attribute", "~> 1.0"
# Appsignal integration
gem "appsignal", "~> 3.0", require: false
gem 'view_component'
# Lookbook
gem 'lookbook', '~> 2.0.5'
gem 'turbo-rails', "~> 1.1"
@@ -234,6 +236,9 @@ group :test do
# XML comparison tests
gem 'compare-xml', '~> 0.66', require: false
# PDF Export tests
gem 'pdf-inspector', '~> 1.2'
# brings back testing for 'assigns' and 'assert_template' extracted in rails 5
gem 'rails-controller-testing', '~> 1.0.2'
@@ -272,10 +277,6 @@ group :development do
gem 'spring'
gem 'spring-commands-rspec'
# Gems for living styleguide
gem 'livingstyleguide', '~> 2.1.0'
gem 'sassc-rails'
gem 'colored2'
# git hooks manager
@@ -352,3 +353,7 @@ gemfiles.each do |file|
# don't use eval_gemfile(file) here as it will break dependabot!
send(:eval_gemfile, file) if File.readable?(file)
end
gem "primer_view_components", git: 'https://github.com/opf/primer_view_components', ref: '18abe4d'
gem "openproject-octicons", '~>19.6.7'
gem "openproject-octicons_helper", '~>19.6.7'
+94 -78
View File
@@ -10,10 +10,10 @@ GIT
GIT
remote: https://github.com/opf/md-to-pdf
revision: 10bbfbd6f4afdb36918ed9ea686c818144683738
tag: v0.0.18
revision: 76945d45c14b841e2312f992422e2631a4524114
ref: 76945d45c14b841e2312f992422e2631a4524114
specs:
md_to_pdf (0.0.18)
md_to_pdf (0.0.19)
commonmarker (~> 0.23)
front_matter_parser (~> 1.0)
json-schema (~> 4.0)
@@ -44,12 +44,23 @@ GIT
GIT
remote: https://github.com/opf/omniauth-openid_connect-providers.git
revision: a6c0c3ed78fac79cf4d007e40d4029e524ec7751
ref: a6c0c3ed78fac79cf4d007e40d4029e524ec7751
revision: 7559f44e70203f94572a90e1b4d1d1f8279cd40f
ref: 7559f44e70203f94572a90e1b4d1d1f8279cd40f
specs:
omniauth-openid_connect-providers (0.2.0)
omniauth-openid-connect (>= 0.2.1)
GIT
remote: https://github.com/opf/primer_view_components
revision: 18abe4d3c8059bc915443767c033543c2e6e8beb
ref: 18abe4d
specs:
primer_view_components (0.4.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
octicons (>= 18.0.0)
view_component (>= 3.1, < 4.0)
PATH
remote: modules/auth_plugins
specs:
@@ -74,7 +85,6 @@ PATH
specs:
openproject-backlogs (1.0.0)
acts_as_list (~> 1.1.0)
openproject-pdf_export
PATH
remote: modules/bim
@@ -162,13 +172,6 @@ PATH
overviews (1.0.0)
grids
PATH
remote: modules/pdf_export
specs:
openproject-pdf_export (1.0.0)
pdf-inspector (~> 1.3.0)
prawn (~> 2.2)
PATH
remote: modules/recaptcha
specs:
@@ -195,7 +198,7 @@ PATH
remote: modules/two_factor_authentication
specs:
openproject-two_factor_authentication (1.0.0)
aws-sdk-sns (~> 1.61.0)
aws-sdk-sns (~> 1.65.0)
messagebird-rest (~> 1.4.2)
rotp (~> 6.1)
@@ -300,7 +303,7 @@ GEM
activerecord (>= 4.2)
acts_as_tree (2.9.1)
activerecord (>= 3.0.0)
addressable (2.8.4)
addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0)
afm (0.2.2)
@@ -308,7 +311,7 @@ GEM
airbrake-ruby (~> 6.0)
airbrake-ruby (6.2.1)
rbtree3 (~> 0.6)
appsignal (3.4.6)
appsignal (3.4.10)
rack
ast (2.4.2)
attr_required (1.0.1)
@@ -317,21 +320,21 @@ GEM
awesome_nested_set (3.5.0)
activerecord (>= 4.0.0, < 7.1)
aws-eventstream (1.2.0)
aws-partitions (1.782.0)
aws-sdk-core (3.176.1)
aws-partitions (1.796.0)
aws-sdk-core (3.180.1)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.68.0)
aws-sdk-core (~> 3, >= 3.176.0)
aws-sdk-kms (1.71.0)
aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.127.0)
aws-sdk-core (~> 3, >= 3.176.0)
aws-sdk-s3 (1.132.0)
aws-sdk-core (~> 3, >= 3.179.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6)
aws-sdk-sns (1.61.0)
aws-sdk-core (~> 3, >= 3.174.0)
aws-sdk-sns (1.65.0)
aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.6.0)
aws-eventstream (~> 1, >= 1.0.2)
@@ -339,7 +342,7 @@ GEM
bindata (2.4.15)
bootsnap (1.16.0)
msgpack (~> 1.2)
brakeman (6.0.0)
brakeman (6.0.1)
browser (5.3.1)
builder (3.2.4)
byebug (11.1.3)
@@ -369,7 +372,7 @@ GEM
with_advisory_lock (>= 4.0.0)
coderay (1.1.3)
colored2 (3.1.2)
commonmarker (0.23.9)
commonmarker (0.23.10)
compare-xml (0.66)
nokogiri (~> 1.8)
concurrent-ruby (1.2.2)
@@ -377,6 +380,8 @@ GEM
crack (0.4.5)
rexml
crass (1.0.6)
css_parser (1.14.0)
addressable
cuprite (0.14.3)
capybara (~> 3.0)
ferrum (~> 0.13.0)
@@ -445,13 +450,13 @@ GEM
tzinfo
eventmachine (1.2.7)
eventmachine_httpserver (0.2.1)
excon (0.99.0)
excon (0.100.0)
factory_bot (6.2.1)
activesupport (>= 5.0.0)
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
faraday (2.7.9)
faraday (2.7.10)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-follow_redirects (0.3.0)
@@ -490,6 +495,7 @@ GEM
fuubar (2.5.1)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
glob (0.4.0)
globalid (1.1.0)
activesupport (>= 5.0)
gon (6.4.0)
@@ -508,7 +514,7 @@ GEM
webrick
google-apis-gmail_v1 (0.30.0)
google-apis-core (>= 0.11.0, < 2.a)
googleauth (1.6.0)
googleauth (1.7.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@@ -533,7 +539,9 @@ GEM
html-pipeline (2.14.3)
activesupport (>= 2)
nokogiri (>= 1.4)
htmlbeautifier (1.4.2)
htmldiff (0.0.1)
htmlentities (4.3.4)
http-accept (1.7.0)
http-cookie (1.0.5)
domain_name (~> 0.5)
@@ -541,15 +549,16 @@ GEM
httpclient (2.8.3)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
i18n-js (3.9.2)
i18n (>= 0.6.6)
i18n-js (4.2.3)
glob (>= 0.4.0)
i18n
icalendar (2.8.0)
ice_cube (~> 0.16)
ice_cube (0.16.4)
interception (0.5)
io-console (0.6.0)
irb (1.7.0)
reline (>= 0.3.0)
irb (1.7.4)
reline (>= 0.3.6)
iso8601 (0.13.0)
jmespath (1.6.2)
json (2.6.3)
@@ -574,18 +583,12 @@ GEM
language_server-protocol (3.17.0.3)
launchy (2.5.2)
addressable (~> 2.8)
lefthook (1.4.3)
lefthook (1.4.8)
letter_opener (1.8.1)
launchy (>= 2.2, < 3)
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
livingstyleguide (2.1.0)
minisyntax (>= 0.2.5)
redcarpet
sassc
thor
tilt
lobby_boy (0.1.3)
omniauth (~> 1.1)
omniauth-openid-connect (>= 0.2.1)
@@ -598,6 +601,18 @@ GEM
loofah (2.21.3)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
lookbook (2.0.5)
activemodel
css_parser
htmlbeautifier (~> 1.3)
htmlentities (~> 4.3.4)
marcel (~> 1.0)
railties (>= 5.0)
redcarpet (~> 3.5)
rouge (>= 3.26, < 5.0)
view_component (>= 2.0)
yard (~> 0.9.25)
zeitwerk (~> 2.5)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
@@ -615,9 +630,8 @@ GEM
mime-types-data (3.2023.0218.1)
mini_magick (4.12.0)
mini_mime (1.1.2)
mini_portile2 (2.8.2)
minisyntax (0.2.5)
minitest (5.18.1)
mini_portile2 (2.8.4)
minitest (5.19.0)
msgpack (1.7.1)
multi_json (1.15.0)
mustermann (3.0.0)
@@ -636,10 +650,11 @@ GEM
net-protocol
netrc (0.11.0)
nio4r (2.5.9)
nokogiri (1.15.2)
nokogiri (1.15.3)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.15.0)
octicons (19.4.0)
oj (3.15.1)
okcomputer (1.18.4)
omniauth-saml (1.10.3)
omniauth (~> 1.3, >= 1.3.2)
@@ -658,10 +673,15 @@ GEM
validate_email
validate_url
webfinger (~> 2.0)
openproject-octicons (19.6.7)
openproject-octicons_helper (19.6.7)
actionview
openproject-octicons (= 19.6.7)
railties
openproject-token (3.0.1)
activemodel
os (1.1.4)
ox (2.14.16)
ox (2.14.17)
paper_trail (12.3.0)
activerecord (>= 5.2)
request_store (~> 1.1)
@@ -685,7 +705,6 @@ GEM
activesupport (> 2.2.1)
nokogiri (~> 1.10, >= 1.10.4)
rubyzip (>= 1.2.0)
posix-spawn (0.3.15)
prawn (2.4.0)
pdf-core (~> 0.9.0)
ttfunk (~> 1.7)
@@ -704,7 +723,7 @@ GEM
pry (>= 0.12.0)
psych (5.1.0)
stringio
public_suffix (5.0.1)
public_suffix (5.0.3)
puffing-billy (3.1.0)
addressable (~> 2.5)
em-http-request (~> 1.1, >= 1.1.0)
@@ -719,14 +738,14 @@ GEM
puma (>= 5.0, < 7)
raabro (1.4.0)
racc (1.7.1)
rack (2.2.7)
rack (2.2.8)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
rack-cors (2.0.1)
rack (>= 2.0.0)
rack-mini-profiler (3.1.0)
rack-mini-profiler (3.1.1)
rack (>= 1.2.0)
rack-oauth2 (2.2.0)
activesupport
@@ -761,8 +780,9 @@ GEM
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
rails-dom-testing (2.1.1)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
@@ -788,7 +808,7 @@ GEM
recaptcha (5.14.0)
redcarpet (3.6.0)
regexp_parser (2.8.1)
reline (0.3.5)
reline (0.3.6)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
@@ -805,12 +825,12 @@ GEM
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retriable (3.1.2)
rexml (3.2.5)
rexml (3.2.6)
rinku (2.0.6)
roar (1.2.0)
representable (~> 3.1)
rotp (6.2.2)
rouge (4.1.2)
rouge (4.1.3)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
@@ -820,7 +840,7 @@ GEM
rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.5)
rspec-mocks (3.12.6)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.0.3)
@@ -833,8 +853,8 @@ GEM
rspec-support (~> 3.12)
rspec-retry (0.6.2)
rspec-core (> 3.3)
rspec-support (3.12.0)
rubocop (1.54.1)
rspec-support (3.12.1)
rubocop (1.55.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
@@ -842,7 +862,7 @@ GEM
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0)
rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0)
@@ -855,7 +875,7 @@ GEM
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-rspec (2.22.0)
rubocop-rspec (2.23.0)
rubocop (~> 1.33)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
@@ -874,17 +894,9 @@ GEM
rubytree (2.0.2)
json (~> 2.0, > 2.3.1)
rubyzip (2.3.2)
sanitize (6.0.1)
sanitize (6.0.2)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
sassc (2.4.0)
ffi (~> 1.9)
sassc-rails (2.1.2)
railties (>= 4.0.0)
sassc (>= 2.0)
sprockets (> 3.0)
sprockets-rails
tilt
secure_headers (6.5.0)
selenium-webdriver (4.10.0)
rexml (~> 3.2, >= 3.2.5)
@@ -915,6 +927,8 @@ GEM
sprockets (>= 3.0.0)
ssrf_filter (1.1.1)
stackprof (0.2.25)
store_attribute (1.1.1)
activerecord (>= 6.0)
stringex (2.8.6)
stringio (3.0.7)
structured_warnings (0.4.0)
@@ -930,7 +944,6 @@ GEM
test-prof (1.2.2)
text-hyphen (1.5.0)
thor (1.2.2)
tilt (2.1.0)
timecop (0.9.6)
timeout (0.4.0)
trailblazer-option (0.1.2)
@@ -956,7 +969,7 @@ GEM
validate_url (1.0.15)
activemodel (>= 3.0.0)
public_suffix
view_component (3.3.0)
view_component (3.5.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
@@ -986,7 +999,8 @@ GEM
activerecord (>= 4.2)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.8)
yard (0.9.34)
zeitwerk (2.6.11)
PLATFORMS
ruby
@@ -1051,7 +1065,7 @@ DEPENDENCIES
grids!
html-pipeline (~> 2.14.0)
htmldiff
i18n-js (~> 3.9.0)
i18n-js (~> 4.2.3)
json_schemer (~> 1.0.1)
json_spec (~> 1.1.4)
ladle
@@ -1059,8 +1073,8 @@ DEPENDENCIES
lefthook
letter_opener
listen (~> 3.8.0)
livingstyleguide (~> 2.1.0)
lograge (~> 0.12.0)
lookbook (~> 2.0.5)
mail (= 2.8.1)
matrix (~> 0.4.2)
md_to_pdf!
@@ -1088,8 +1102,9 @@ DEPENDENCIES
openproject-job_status!
openproject-ldap_groups!
openproject-meeting!
openproject-octicons (~> 19.6.7)
openproject-octicons_helper (~> 19.6.7)
openproject-openid_connect!
openproject-pdf_export!
openproject-recaptcha!
openproject-reporting!
openproject-storages!
@@ -1102,10 +1117,11 @@ DEPENDENCIES
ox
paper_trail (~> 12.3)
parallel_tests (~> 4.0)
pdf-inspector (~> 1.2)
pg (~> 1.5.0)
plaintext (~> 0.3.2)
posix-spawn (~> 0.3.13)
prawn (~> 2.2)
primer_view_components!
pry-byebug (~> 3.10.0)
pry-rails (~> 0.3.6)
pry-rescue (~> 1.5.2)
@@ -1140,8 +1156,7 @@ DEPENDENCIES
ruby-prof
ruby-progressbar (~> 1.13.0)
rubytree (~> 2.0.0)
sanitize (~> 6.0.1)
sassc-rails
sanitize (~> 6.0.2)
secure_headers (~> 6.5.0)
selenium-webdriver (~> 4.0)
semantic (~> 1.6.1)
@@ -1152,6 +1167,7 @@ DEPENDENCIES
sprockets (~> 3.7.2)
sprockets-rails (~> 3.4.2)
stackprof
store_attribute (~> 1.0)
stringex (~> 2.8.5)
structured_warnings (~> 0.4.0)
svg-graph (~> 2.2.0)
+1 -2
View File
@@ -10,7 +10,7 @@ end
gem 'omniauth-openid_connect-providers',
git: 'https://github.com/opf/omniauth-openid_connect-providers.git',
ref: 'a6c0c3ed78fac79cf4d007e40d4029e524ec7751'
ref: '7559f44e70203f94572a90e1b4d1d1f8279cd40f'
gem 'omniauth-openid-connect',
git: 'https://github.com/opf/omniauth-openid-connect.git',
@@ -28,7 +28,6 @@ group :opf_plugins do
gem 'costs', path: 'modules/costs'
gem 'openproject-reporting', path: 'modules/reporting'
gem 'openproject-meeting', path: 'modules/meeting'
gem 'openproject-pdf_export', path: 'modules/pdf_export'
gem "openproject-backlogs", path: 'modules/backlogs'
gem 'openproject-avatars', path: 'modules/avatars'
gem 'openproject-two_factor_authentication', path: 'modules/two_factor_authentication'
+56
View File
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 500 500"
version="1.1"
id="svg861"
sodipodi:docname="icon_logo.svg"
inkscape:version="1.0.2 (e86c8708, 2021-01-15)">
<metadata
id="metadata867">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs865" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1792"
inkscape:window-height="1067"
id="namedview863"
showgrid="false"
inkscape:zoom="1.346"
inkscape:cx="250"
inkscape:cy="250"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg861" />
<path
d="m 267.98948,387.74784 v 0 a 4.9312711,5.0968082 0 0 0 0,0.66712 28.761445,29.726934 0 0 0 57.49708,0 3.3047262,3.415662 0 0 0 0,-0.74718 v 0 -71.16852 h -57.49708 z"
style="fill:#0770b8;stroke-width:2.62479"
id="path857" />
<path
d="M 402.16653,3.4858609 H 363.82654 A 95.346514,98.547187 0 0 0 267.98948,101.95299 V 221.39442 H 99.319355 A 95.346514,98.547187 0 0 0 3.4822955,319.86155 v 79.25404 a 96.327605,99.561212 0 0 0 95.8370595,99.64126 h 38.339985 a 96.327605,99.561212 0 0 0 95.83706,-99.64126 v -79.25404 c 0,-1.30756 0,-3.36229 0,-3.36229 h -57.49707 v 82.72307 a 38.882169,40.187399 0 0 1 -38.33999,40.21408 H 99.319355 A 38.882169,40.187399 0 0 1 60.979367,399.22233 V 319.96829 A 37.901078,39.173374 0 0 1 99.319355,280.92834 H 402.16653 a 96.301787,99.534527 0 0 0 95.83706,-99.61458 V 101.95299 A 95.346514,98.547187 0 0 0 402.16653,3.4858609 Z M 440.50652,181.20702 a 38.856351,40.160714 0 0 1 -38.33999,40.1874 H 325.61565 V 101.95299 a 37.901078,39.173374 0 0 1 38.33998,-39.039947 h 38.33999 a 37.901078,39.173374 0 0 1 38.2109,39.039947 z"
style="fill:#0770b8;stroke-width:2.62479"
id="path859" />
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M267.99 387.748a4.931 5.097 0 0 0 0 .667 28.761 29.727 0 0 0 57.497 0 3.305 3.416 0 0 0 0-.747v-71.169h-57.498z"
style="fill:#fff;stroke-width:2.62479"/>
<path d="M402.167 3.486h-38.34a95.347 98.547 0 0 0-95.838 98.467v119.441H99.32a95.347 98.547 0 0 0-95.837 98.468v79.254a96.328 99.561 0 0 0 95.837 99.64h38.34a96.328 99.561 0 0 0 95.837-99.64v-82.617H176v82.723a38.882 40.187 0 0 1-38.34 40.214H99.32a38.882 40.187 0 0 1-38.34-40.214v-79.254a37.901 39.173 0 0 1 38.34-39.04h302.848a96.302 99.535 0 0 0 95.837-99.614v-79.361a95.347 98.547 0 0 0-95.837-98.467Zm38.34 177.721a38.856 40.16 0 0 1-38.34 40.187h-76.551V101.953a37.901 39.173 0 0 1 38.34-39.04h38.34a37.901 39.173 0 0 1 38.21 39.04z"
style="fill:#fff;stroke-width:2.62479"/>
</svg>

After

Width:  |  Height:  |  Size: 836 B

@@ -1 +0,0 @@
@import ../../../frontend/src/global_styles/**/_*.lsg
@@ -0,0 +1,10 @@
<li class="<%= li_css_class %>">
<a href="<%= dynamic_path %>"
id="<%= id %>"
title="<%= title %>"
arial-label="<%= aria_label %>"
class="<%= link_css_class %>">
<%= icon %>
<%= label %>
</a>
</li>
+80
View File
@@ -0,0 +1,80 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2023 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 AddButtonComponent < ApplicationComponent
options :current_project
def render?
raise 'Implement the conditions for which the component should render or not'
end
def dynamic_path
raise "Implement the path for this component's href"
end
def id
raise "Implement the id for this component"
end
def li_css_class
'toolbar-item'
end
def title
accessibility_label_text
end
def label
content_tag(:span,
label_text,
class: 'button--text')
end
def aria_label
accessibility_label_text
end
def accessibility_label_text
raise "Specify the aria label and title text to be used for this component"
end
def label_text
raise "Specify the label text to be used for this component"
end
def link_css_class
'button -alt-highlight'
end
def icon
helpers.op_icon('button--icon icon-add')
end
end
+1 -1
View File
@@ -55,7 +55,7 @@ class ApplicationComponent < ViewComponent::Base
names.each do |name|
define_method(name) do
options[name] || default_values[name]
options.has_key?(name) ? options[name] : default_values[name]
end
end
end
@@ -30,9 +30,11 @@ See COPYRIGHT and LICENSE files for more details.
<div class="attributes-group">
<%= header %>
<div class="attributes-key-value">
<div class="attributes-group--attributes">
<% attributes.each do |attribute| %>
<%= attribute %>
<div class="attributes-key-value">
<%= attribute %>
</div>
<% end %>
</div>
@@ -30,6 +30,10 @@ See COPYRIGHT and LICENSE files for more details.
<div class="attributes-key-value--key"><%= key %></div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span><%= value %></span>
<% if value %>
<span><%= value %></span>
<% end %>
<%= content %>
</div>
</div>
@@ -0,0 +1,71 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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 LdapAuthSources
class RowComponent < ::RowComponent
def name
content = link_to model.name, edit_ldap_auth_source_path(model)
if model.seeded_from_env?
content += helpers.op_icon('icon icon-info2', title: I18n.t(:label_seeded_from_env_warning))
end
content
end
delegate :host, to: :model
def users
model.users.size
end
def row_css_id
"ldap-auth-source-#{model.id}"
end
def button_links
[test_link, delete_link].compact
end
def test_link
link_to t(:button_test), { controller: 'ldap_auth_sources', action: 'test_connection', id: model }
end
def delete_link
return if users > 0
link_to I18n.t(:button_delete),
{ controller: 'ldap_auth_sources', id: model.id, action: :destroy },
method: :delete,
data: { confirm: I18n.t(:text_are_you_sure) },
class: 'icon icon-delete',
title: I18n.t(:button_delete)
end
end
end
@@ -0,0 +1,63 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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 LdapAuthSources
class TableComponent < ::TableComponent
columns :name, :host, :users
def initial_sort
%i[id asc]
end
def sortable?
true
end
def sortable_column?(_column)
false
end
def inline_create_link
link_to(new_ldap_auth_source_path,
class: 'budget-add-row wp-inline-create--add-link',
title: I18n.t(:label_ldap_auth_source_new)) do
helpers.op_icon('icon icon-add')
end
end
def headers
[
['name', { caption: LdapAuthSource.human_attribute_name('name') }],
['host', { caption: LdapAuthSource.human_attribute_name('host') }],
['users', { caption: I18n.t(:label_user_plural) }]
]
end
end
end
@@ -59,7 +59,7 @@ module OAuth
def client_credentials
if user_id = application.client_credentials_user_id
link_to_user User.find(user_id)
helpers.link_to_user User.find(user_id)
else
'-'
end
+2 -1
View File
@@ -45,7 +45,8 @@ module Users
def login
icon = helpers.avatar user, size: :mini
link = link_to h(user.login), edit_user_path(user)
link = link_to h(user.login), helpers.allowed_management_user_profile_path(user)
icon + link
end
+1 -1
View File
@@ -28,6 +28,6 @@
module Groups
class DeleteContract < ::DeleteContract
delete_permission -> { user.active? && user.admin? }
delete_permission(:admin)
end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'queries/create_contract'
module Queries
class GlobalCreateContract < CreateContract
validate :validate_project_present
private
def validate_project_present
errors.add :project_id, :blank if model.project_id.blank?
end
end
end
@@ -26,8 +26,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject
module PDFExport
require "open_project/pdf_export/engine"
module Roles
class DeleteContract < ::DeleteContract
delete_permission(:admin)
end
end
+8 -5
View File
@@ -31,7 +31,10 @@ module Users
include AssignableCustomFieldValues
attribute :login,
writable: ->(*) { user.allowed_to_globally?(:manage_user) && model.id != user.id }
writable: ->(*) {
(user.allowed_to_globally?(:manage_user) || user.allowed_to_globally?(:create_user)) &&
model.id != user.id
}
attribute :firstname
attribute :lastname
attribute :mail
@@ -39,11 +42,11 @@ module Users
writable: ->(*) { user.admin? && model.id != user.id }
attribute :language
attribute :auth_source_id,
writable: ->(*) { user.allowed_to_globally?(:manage_user) }
attribute :ldap_auth_source_id,
writable: ->(*) { user.allowed_to_globally?(:manage_user) || user.allowed_to_globally?(:create_user) }
attribute :status,
writable: ->(*) { user.allowed_to_globally?(:manage_user) }
writable: ->(*) { user.allowed_to_globally?(:manage_user) || user.allowed_to_globally?(:create_user) }
attribute :identity_url,
writable: ->(*) { user.admin? }
@@ -88,7 +91,7 @@ module Users
# rubocop:disable Rails/DynamicFindBy
def existing_auth_source
if auth_source_id && AuthSource.find_by_unique(auth_source_id).nil?
if ldap_auth_source_id && LdapAuthSource.find_by_unique(ldap_auth_source_id).nil?
errors.add :auth_source, :error_not_found
end
end
+3 -3
View File
@@ -62,14 +62,14 @@ module Users
end
def no_auth?
model.password.blank? && model.auth_source_id.blank? && model.identity_url.blank?
model.password.blank? && model.ldap_auth_source_id.blank? && model.identity_url.blank?
end
##
# Users can only be created by Admins or users with
# the global right to :manage_user
# the global right to :create_user
def user_allowed_to_add
unless user.allowed_to_globally?(:manage_user)
unless user.allowed_to_globally?(:create_user)
errors.add :base, :error_unauthorized
end
end
+1 -1
View File
@@ -35,7 +35,7 @@ module Users
##
# Users can only be updated when
# - the user is editing herself
# - the user has the global manage_user CRU permission
# - the user has the global manage_user permission
# - the user is an admin
def user_allowed_to_update
unless user == model || user.allowed_to_globally?(:manage_user)
+7 -13
View File
@@ -41,7 +41,6 @@ class AccountController < ApplicationController
before_action :disable_api
before_action :check_auth_source_sso_failure, only: :auth_source_sso_failed
before_action :check_internal_login_enabled, only: :internal_login
after_action :remove_internal_login_flag, only: :login
layout 'no_menu'
@@ -51,7 +50,7 @@ class AccountController < ApplicationController
if user.logged?
redirect_after_login(user)
elsif omniauth_direct_login? && !session[:internal_login]
elsif request.get? && omniauth_direct_login?
direct_login(user)
elsif request.post?
authenticate_user
@@ -59,8 +58,7 @@ class AccountController < ApplicationController
end
def internal_login
session[:internal_login] = true
redirect_to action: :login
render 'account/login'
end
# Log out current user and redirect to welcome page
@@ -227,7 +225,7 @@ class AccountController < ApplicationController
session[:invitation_token] = token.value
user = token.user
if user.auth_source && user.auth_source.auth_method_name == 'LDAP'
if user.ldap_auth_source
activate_through_ldap user
else
activate_user user
@@ -249,7 +247,7 @@ class AccountController < ApplicationController
def activate_through_ldap(user)
session[:auth_source_registration] = {
login: user.login,
auth_source_id: user.auth_source_id
ldap_auth_source_id: user.ldap_auth_source_id
}
flash[:notice] = I18n.t('account.auth_source_login', login: user.login).html_safe
@@ -278,7 +276,7 @@ class AccountController < ApplicationController
user = find_or_create_sso_user(login, save: false)
if user.try(:new_record?)
return onthefly_creation_failed user, login: user.login, auth_source_id: user.auth_source_id
return onthefly_creation_failed user, login: user.login, ldap_auth_source_id: user.ldap_auth_source_id
end
show_sso_error_for user
@@ -362,7 +360,7 @@ class AccountController < ApplicationController
user.attributes = permitted_params.user
user.activate
user.login = session[:auth_source_registration][:login]
user.auth_source_id = session[:auth_source_registration][:auth_source_id]
user.ldap_auth_source_id = session[:auth_source_registration][:ldap_auth_source_id]
respond_for_registered_user(user)
end
@@ -439,7 +437,7 @@ class AccountController < ApplicationController
flash_and_log_invalid_credentials
end
elsif user.new_record?
onthefly_creation_failed(user, login: user.login, auth_source_id: user.auth_source_id)
onthefly_creation_failed(user, login: user.login, ldap_auth_source_id: user.ldap_auth_source_id)
else
# Valid user
successful_authentication(user)
@@ -538,8 +536,4 @@ class AccountController < ApplicationController
def check_internal_login_enabled
render_404 unless omniauth_direct_login?
end
def remove_internal_login_flag
session.delete :internal_login
end
end
+6 -1
View File
@@ -90,6 +90,11 @@ class ActivitiesController < ApplicationController
end
def determine_subprojects
# In OP < 13.0 session[:activity] was an Array.
# If such a session is still present, we need to reset it.
# This line can probably be removed in OP 14.0.
session[:activity] = nil unless session[:activity].is_a?(Hash)
@with_subprojects = if params[:with_subprojects].nil? &&
(session[:activity].nil? || session[:activity][:with_subprojects].nil?)
Setting.display_subprojects_work_packages?
@@ -105,7 +110,7 @@ class ActivitiesController < ApplicationController
end
def respond_html
render locals: { menu_name: project_or_global_activity_menu }
render locals: { menu_name: project_or_global_menu }
end
def respond_atom
@@ -40,10 +40,6 @@ module Admin::Settings
t(:label_date_format)
end
def show_local_breadcrumb
true
end
private
def validate_start_of_week_and_first_week_of_year_combination
@@ -52,7 +48,7 @@ module Admin::Settings
if start_of_week.present? ^ start_of_year.present?
flash[:error] = I18n.t(
'settings.display.first_date_of_week_and_year_set',
'settings.date_format.first_date_of_week_and_year_set',
first_week_setting_name: I18n.t(:setting_first_week_of_year),
day_of_week_setting_name: I18n.t(:setting_start_of_week)
)
@@ -26,10 +26,12 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module Roles::NotifyMixin
private
module Admin::Settings
class IcalendarSettingsController < ::Admin::SettingsController
menu_item :icalendar
def notify_changed_roles(action, changed_role)
OpenProject::Notifications.send(:roles_changed, action:, role: changed_role)
def default_breadcrumb
t(:label_calendar_subscriptions)
end
end
end
@@ -34,10 +34,6 @@ module Admin::Settings
t(:label_working_days)
end
def show_local_breadcrumb
true
end
def failure_callback(call)
@modified_non_working_days = modified_non_working_days_for(call.result)
flash[:error] = call.message || I18n.t(:notice_internal_server_error)
-122
View File
@@ -1,122 +0,0 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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 AuthSourcesController < ApplicationController
include PaginationHelper
layout 'admin'
before_action :require_admin
before_action :block_if_password_login_disabled
def index
@auth_sources = AuthSource
.order(id: :asc)
.page(page_param)
.per_page(per_page_param)
render 'auth_sources/index'
end
def new
@auth_source = auth_source_class.new
render 'auth_sources/new'
end
def edit
@auth_source = AuthSource.find(params[:id])
render 'auth_sources/edit'
end
def create
@auth_source = auth_source_class.new permitted_params.auth_source
if @auth_source.save
flash[:notice] = I18n.t(:notice_successful_create)
redirect_to action: 'index'
else
render 'auth_sources/new'
end
end
def update
@auth_source = AuthSource.find(params[:id])
updated = permitted_params.auth_source
updated.delete :account_password if updated[:account_password].blank?
if @auth_source.update updated
flash[:notice] = I18n.t(:notice_successful_update)
redirect_to action: 'index'
else
render 'auth_sources/edit'
end
end
def test_connection
@auth_method = AuthSource.find(params[:id])
begin
@auth_method.test_connection
flash[:notice] = I18n.t(:notice_successful_connection)
rescue StandardError => e
flash[:error] = I18n.t(:error_unable_to_connect, value: e.message)
end
redirect_to action: 'index'
end
def destroy
@auth_source = AuthSource.find(params[:id])
if @auth_source.users.empty?
@auth_source.destroy
flash[:notice] = t(:notice_successful_delete)
else
flash[:warning] = t(:notice_wont_delete_auth_source)
end
redirect_to action: 'index'
end
protected
def auth_source_class
AuthSource
end
def default_breadcrumb
if action_name == 'index'
t(:label_auth_source_plural)
else
ActionController::Base.helpers.link_to(t(:label_auth_source_plural), ldap_auth_sources_path)
end
end
def show_local_breadcrumb
true
end
def block_if_password_login_disabled
render_404 if OpenProject::Configuration.disable_password_login?
end
end
+4 -4
View File
@@ -67,7 +67,7 @@ class ColorsController < ApplicationController
flash[:notice] = I18n.t(:notice_successful_create)
redirect_to colors_path
else
flash.now[:error] = I18n.t('timelines.color_could_not_be_saved')
flash.now[:error] = I18n.t(:error_color_could_not_be_saved)
render action: 'new'
end
end
@@ -79,7 +79,7 @@ class ColorsController < ApplicationController
flash[:notice] = I18n.t(:notice_successful_update)
redirect_to colors_path
else
flash.now[:error] = I18n.t('timelines.color_could_not_be_saved')
flash.now[:error] = I18n.t(:error_color_could_not_be_saved)
render action: 'edit'
end
end
@@ -103,9 +103,9 @@ class ColorsController < ApplicationController
def default_breadcrumb
if action_name == 'index'
t('timelines.admin_menu.colors')
t(:label_color_plural)
else
ActionController::Base.helpers.link_to(t('timelines.admin_menu.colors'), colors_path)
ActionController::Base.helpers.link_to(t(:label_color_plural), colors_path)
end
end
+3 -3
View File
@@ -117,12 +117,12 @@ module AuthSourceSSO
def find_user_from_auth_source(login)
User
.by_login(login)
.where.not(auth_source_id: nil)
.where.not(ldap_auth_source_id: nil)
.first
end
def create_user_from_auth_source(login, save:)
attrs = AuthSource.find_user(login)
attrs = LdapAuthSource.find_user(login)
return unless attrs
attrs[:login] = login
@@ -143,7 +143,7 @@ module AuthSourceSSO
call.on_success do
logger.info(
"User '#{user.login}' created from external auth source: " +
"#{user.auth_source.type} - #{user.auth_source.name}"
"#{user.ldap_auth_source.type} - #{user.ldap_auth_source.name}"
)
end
+2 -10
View File
@@ -40,19 +40,11 @@ module Layout
end
end
def project_or_global_wp_query_menu
def project_or_global_menu
if @project
:project_menu
else
:global_work_packages_menu
end
end
def project_or_global_activity_menu
if @project
:project_menu
else
:global_activities_menu
:global_menu
end
end
end
+5 -1
View File
@@ -29,7 +29,7 @@
class HomescreenController < ApplicationController
skip_before_action :check_if_login_required, only: [:robots]
layout 'no_menu'
layout 'global'
def index
@newest_projects = Project.visible.newest.take(3)
@@ -40,6 +40,10 @@ class HomescreenController < ApplicationController
@homescreen = OpenProject::Static::Homescreen
end
current_menu_item [:index] do
:home
end
def robots
if Setting.login_required?
render template: 'homescreen/robots-login-required', format: :text
@@ -26,12 +26,99 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class LdapAuthSourcesController < AuthSourcesController
class LdapAuthSourcesController < ApplicationController
menu_item :ldap_authentication
include PaginationHelper
layout 'admin'
before_action :require_admin
before_action :block_if_password_login_disabled
self._model_object = LdapAuthSource
before_action :find_model_object, only: %i(edit update destroy)
before_action :prevent_editing_when_seeded, only: %i(update)
def index
@ldap_auth_sources = LdapAuthSource
.order(id: :asc)
.page(page_param)
.per_page(per_page_param)
end
def new
@ldap_auth_source = LdapAuthSource.new
end
def edit; end
def create
@ldap_auth_source = LdapAuthSource.new permitted_params.ldap_auth_source
if @ldap_auth_source.save
flash[:notice] = I18n.t(:notice_successful_create)
redirect_to action: 'index'
else
render 'new'
end
end
def update
@ldap_auth_source = LdapAuthSource.find(params[:id])
updated = permitted_params.ldap_auth_source
updated.delete :account_password if updated[:account_password].blank?
if @ldap_auth_source.update updated
flash[:notice] = I18n.t(:notice_successful_update)
redirect_to action: 'index'
else
render 'edit'
end
end
def test_connection
@auth_method = LdapAuthSource.find(params[:id])
begin
@auth_method.test_connection
flash[:notice] = I18n.t(:notice_successful_connection)
rescue StandardError => e
flash[:error] = I18n.t(:error_unable_to_connect, value: e.message)
end
redirect_to action: 'index'
end
def destroy
@ldap_auth_source = LdapAuthSource.find(params[:id])
if @ldap_auth_source.users.empty?
@ldap_auth_source.destroy
flash[:notice] = t(:notice_successful_delete)
else
flash[:warning] = t(:notice_wont_delete_auth_source)
end
redirect_to action: 'index'
end
protected
def auth_source_class
LdapAuthSource
def prevent_editing_when_seeded
if @ldap_auth_source.seeded_from_env?
flash[:warning] = I18n.t(:label_seeded_from_env_warning)
redirect_to action: :index
end
end
def default_breadcrumb
if action_name == 'index'
t(:label_ldap_auth_source_plural)
else
ActionController::Base.helpers.link_to(t(:label_ldap_auth_source_plural), ldap_auth_sources_path)
end
end
def show_local_breadcrumb
true
end
def block_if_password_login_disabled
render_404 if OpenProject::Configuration.disable_password_login?
end
end
+6 -6
View File
@@ -147,7 +147,7 @@ class MembersController < ApplicationController
end
def suggest_invite_via_email?(user, query, principals)
user.allowed_to_globally?(:manage_user) &&
user.allowed_to_globally?(:create_user) &&
query =~ mail_regex &&
principals.none? { |p| p.mail == query || p.login == query } &&
query # finally return email
@@ -188,7 +188,7 @@ class MembersController < ApplicationController
def create_members
overall_result = nil
with_new_member_params do |member_params|
each_new_member_param do |member_params|
service_call = Members::CreateService
.new(user: current_user)
.call(member_params)
@@ -203,7 +203,7 @@ class MembersController < ApplicationController
overall_result
end
def with_new_member_params
def each_new_member_param
user_ids = user_ids_for_new_members(params[:member])
group_ids = Group.where(id: user_ids).pluck(:id)
@@ -222,8 +222,8 @@ class MembersController < ApplicationController
def invite_new_users(user_ids)
user_ids.map do |id|
if id.to_i == 0 && id.present? # we've got an email - invite that user
# Only users with the manage_member permission can add users.
if current_user.allowed_to_globally?(:manage_user) && enterprise_allow_new_users?
# Only users with the create_user permission can add users.
if current_user.allowed_to_globally?(:create_user) && enterprise_allow_new_users?
# The invitation can pretty much only fail due to the user already
# having been invited. So look them up if it does.
user = UserInvitation.invite_new_user(email: id) ||
@@ -278,7 +278,7 @@ class MembersController < ApplicationController
end
def no_create_errors?(members)
members.present? && members.map(&:errors).select(&:any?).empty?
members.present? && members.map(&:errors).none?(&:any?)
end
def sort_by_groups_last(members)
+41 -6
View File
@@ -78,7 +78,26 @@ class MyController < ApplicationController
end
# Administer access tokens
def access_token; end
def access_token
@storage_tokens = OAuthClientToken
.preload(:oauth_client)
.joins(:oauth_client)
.where(user: @user, oauth_client: { integration_type: 'Storages::Storage' })
end
def delete_storage_token
token = OAuthClientToken
.preload(:oauth_client)
.joins(:oauth_client)
.where(user: @user, oauth_client: { integration_type: 'Storages::Storage' }).find_by(id: params[:id])
if token&.destroy
flash[:info] = I18n.t('my.access_tokens.storages.removed')
else
flash[:error] = I18n.t('my.access_tokens.storages.failed')
end
redirect_to action: :access_token
end
# Configure user's in app notifications
def notifications
@@ -101,9 +120,7 @@ class MyController < ApplicationController
def generate_rss_key
token = Token::RSS.create!(user: current_user)
flash[:info] = [
# rubocop:disable Rails/OutputSafety
t('my.access_token.notice_reset_token', type: 'RSS').html_safe,
# rubocop:enable Rails/OutputSafety
content_tag(:strong, token.plain_value),
t('my.access_token.token_value_warning')
]
@@ -114,13 +131,21 @@ class MyController < ApplicationController
redirect_to action: 'access_token'
end
def revoke_rss_key
current_user.rss_token.destroy
flash[:info] = t('my.access_token.notice_rss_token_revoked')
rescue StandardError => e
Rails.logger.error "Failed to revoke rss token ##{current_user.id}: #{e}"
flash[:error] = t('my.access_token.failed_to_reset_token', error: e.message)
ensure
redirect_to action: 'access_token'
end
# Create a new API key
def generate_api_key
token = Token::API.create!(user: current_user)
flash[:info] = [
# rubocop:disable Rails/OutputSafety
t('my.access_token.notice_reset_token', type: 'API').html_safe,
# rubocop:enable Rails/OutputSafety
content_tag(:strong, token.plain_value),
t('my.access_token.token_value_warning')
]
@@ -131,6 +156,16 @@ class MyController < ApplicationController
redirect_to action: 'access_token'
end
def revoke_api_key
current_user.api_token.destroy
flash[:info] = t('my.access_token.notice_api_token_revoked')
rescue StandardError => e
Rails.logger.error "Failed to revoke api token ##{current_user.id}: #{e}"
flash[:error] = t('my.access_token.failed_to_reset_token', error: e.message)
ensure
redirect_to action: 'access_token'
end
def revoke_ical_token
message = ical_destroy_info_message
@ical_token.destroy
@@ -217,7 +252,7 @@ class MyController < ApplicationController
def ical_destroy_info_message
t(
'my.access_token.notice_ical_tokens_reverted',
'my.access_token.notice_ical_token_revoked',
token_name: @ical_token.ical_token_query_assignment.name,
calendar_name: @ical_token.query.name,
project_name: @ical_token.query.project.name
+1 -1
View File
@@ -48,7 +48,7 @@ class NewsController < ApplicationController
respond_to do |format|
format.html do
render layout: layout_non_or_no_menu
render locals: { menu_name: project_or_global_menu }
end
format.atom do
render_feed(@newss,
@@ -48,10 +48,15 @@ class Projects::TemplatedController < ApplicationController
if service_call.success?
flash[:notice] = t(:notice_successful_update)
redirect_to project_settings_general_path(@project)
else
@errors = service_call.errors
render template: 'projects/settings/general'
messages = [
t('activerecord.errors.template.header', model: Project.model_name.human, count: service_call.errors.count),
service_call.message
]
flash[:error] = messages.join(". ")
end
redirect_to project_settings_general_path(@project)
end
end
+2 -2
View File
@@ -41,7 +41,7 @@ class ProjectsController < ApplicationController
include ProjectsHelper
current_menu_item :index do
:list_projects
:projects
end
def index
@@ -56,7 +56,7 @@ class ProjectsController < ApplicationController
respond_to do |format|
format.html do
render layout: 'no_menu'
render layout: 'global'
end
format.any(*supported_export_formats) do
+11 -15
View File
@@ -28,7 +28,6 @@
class RolesController < ApplicationController
include PaginationHelper
include Roles::NotifyMixin
layout 'admin'
@@ -54,7 +53,7 @@ class RolesController < ApplicationController
end
def create
@call = create_role
@call = Roles::CreateService.new(user: current_user).call(create_params)
@role = @call.result
if @call.success?
@@ -80,13 +79,16 @@ class RolesController < ApplicationController
end
def destroy
@role = Role.find(params[:id])
@role.destroy
flash[:notice] = I18n.t(:notice_successful_delete)
redirect_to action: 'index'
notify_changed_roles(:removed, @role)
rescue StandardError
flash[:error] = I18n.t(:error_can_not_remove_role)
service_result = Roles::DeleteService.new(
model: Role.find(params[:id]),
user: current_user
).call
if service_result.success?
flash[:notice] = I18n.t(:notice_successful_delete)
else
flash[:error] = I18n.t(:error_can_not_remove_role)
end
redirect_to action: 'index'
end
@@ -148,12 +150,6 @@ class RolesController < ApplicationController
end
end
def create_role
Roles::CreateService
.new(user: current_user)
.call(create_params)
end
def roles_scope
Role.order(Arel.sql('builtin, position'))
end
+2 -2
View File
@@ -99,11 +99,11 @@ class TypesController < ApplicationController
if @type.update(permitted_params.type_move)
flash[:notice] = I18n.t(:notice_successful_update)
redirect_to types_path
else
flash.now[:error] = t('type_could_not_be_saved')
flash.now[:error] = I18n.t(:error_type_could_not_be_saved)
render action: 'edit'
end
redirect_to types_path
end
def destroy
+10 -5
View File
@@ -72,7 +72,7 @@ class UsersController < ApplicationController
if can_show_user?
@events = events
render layout: 'no_menu'
render layout: (can_manage_or_create_users? ? 'admin' : 'no_menu')
else
render_404
end
@@ -96,7 +96,7 @@ class UsersController < ApplicationController
if call.success?
flash[:notice] = I18n.t(:notice_successful_create)
redirect_to(params[:continue] ? new_user_path : edit_user_path(@user))
redirect_to(params[:continue] ? new_user_path : helpers.allowed_management_user_profile_path(@user))
else
@errors = call.errors
render action: 'new'
@@ -207,7 +207,7 @@ class UsersController < ApplicationController
flash[:error] = I18n.t(:notice_internal_server_error, app_title: Setting.app_title)
end
redirect_to edit_user_path(@user)
redirect_to helpers.allowed_management_user_profile_path(@user)
end
def destroy
@@ -232,13 +232,18 @@ class UsersController < ApplicationController
private
def can_show_user?
return true if current_user.allowed_to_globally?(:manage_user)
return true if can_manage_or_create_users?
return true if @user == User.current
(@user.active? || @user.registered?) \
&& (@memberships.present? || events.present?)
end
def can_manage_or_create_users?
current_user.allowed_to_globally?(:manage_user) ||
current_user.allowed_to_globally?(:create_user)
end
def events
@events ||= Activities::Fetcher.new(User.current, author: @user).events(limit: 10)
end
@@ -303,7 +308,7 @@ class UsersController < ApplicationController
end
def show_local_breadcrumb
action_name != 'show'
can_manage_or_create_users?
end
def build_user_update_params
+2 -2
View File
@@ -45,7 +45,7 @@ class WorkPackagesController < ApplicationController
respond_to do |format|
format.html do
render :index,
locals: { query: @query, project: @project, menu_name: project_or_global_wp_query_menu },
locals: { query: @query, project: @project, menu_name: project_or_global_menu },
layout: 'angular/angular'
end
@@ -63,7 +63,7 @@ class WorkPackagesController < ApplicationController
respond_to do |format|
format.html do
render :show,
locals: { work_package:, menu_name: project_or_global_wp_query_menu },
locals: { work_package:, menu_name: project_or_global_menu },
layout: 'angular/angular'
end
+1 -1
View File
@@ -26,7 +26,7 @@ module AccountsHelper
# is active. However, if an auth source is present, do show it independently from
# the `email_login` setting as we can't say if the auth source's login is the email address.
def registration_show_email?
!Setting.email_login? || @user.auth_source_id.present?
!Setting.email_login? || @user.ldap_auth_source_id.present? # rubocop:disable Rails/HelperInstanceVariable
end
def registration_footer
+33 -14
View File
@@ -130,21 +130,26 @@ module ApplicationHelper
end
def render_flash_message(type, message, html_options = {})
css_classes = ["flash #{type} icon icon-#{type}", html_options.delete(:class)]
# Add autohide class to notice flashes if configured
if type.to_s == 'notice' && User.current.pref.auto_hide_popups?
css_classes << 'autohide-toaster'
if type.to_s == 'notice'
type = 'success'
end
html_options = { class: css_classes.join(' '), role: 'alert' }.merge(html_options)
content_tag :div, html_options do
concat(join_flash_messages(message))
concat(content_tag(:i, '', class: 'icon-close close-handler',
tabindex: '0',
role: 'button',
aria: { label: ::I18n.t('js.close_popup_title') }))
toast_css_classes = ["op-toast -#{type}", html_options.delete(:class)]
# Add autohide class to notice flashes if configured
if type.to_s == 'success' && User.current.pref.auto_hide_popups?
toast_css_classes << 'autohide-toaster'
end
html_options = { class: toast_css_classes.join(' '), role: 'alert' }.merge(html_options)
close_button = content_tag :a, '', class: 'op-toast--close icon-context icon-close',
title: I18n.t('js.close_popup_title'),
tabindex: '0'
toast = content_tag(:div, join_flash_messages(message), class: 'op-toast--content')
content_tag :div, '', class: 'op-toast--wrapper' do
content_tag :div, '', class: 'op-toast--casing' do
content_tag :div, html_options do
concat(close_button)
concat(toast)
end
end
end
end
@@ -310,6 +315,20 @@ module ApplicationHelper
.sort_by(&:last)
end
def theme_options_for_select
[
[t('themes.light'), 'light'],
[t('themes.light_high_contrast'), 'light_high_contrast'],
[t('themes.dark'), 'dark'],
[t('themes.dark_dimmed'), 'dark_dimmed'],
[t('themes.dark_high_contrast'), 'dark_high_contrast']
]
end
def user_theme_data_attributes
mode, _theme_suffix = User.current.pref.theme.split("_", 2)
"data-color-mode=#{mode} data-#{mode}-theme=#{User.current.pref.theme}"
end
def highlight_default_language(lang_options)
lang_options.map do |(language_name, code)|
if code == Setting.default_language
-29
View File
@@ -1,29 +0,0 @@
module CrowdinHelper
def crowdin_in_context_translation
return unless OpenProject::Configuration.crowdin_in_context_translations?
return unless ::I18n.locale == :lol
# Enable CSP to load the following script by whitelisting for this request.
# This will be slower than manually adding it to the initializer, but we wouldn't want to
# allow cdn.crowdin.com for users without in context translations.
controller.append_content_security_policy_directives(
# initial script and setup API calls
script_src: %w(cdn.crowdin.com crowdin.com),
# Form action to crowdin, github etc.
form_action: %w[https://crowdin.com
https://accounts.google.com
https://api.twitter.com
https://github.com
https://gitlab.com],
# Iframe
frame_src: %w(crowdin.com),
# CSS loaded from cdn
style_src: %w(cdn.crowdin.com)
)
concat(nonced_javascript_tag do
"var _jipt = []; _jipt.push(['project', 'openproject']);".html_safe
end)
concat javascript_include_tag 'https://cdn.crowdin.com/jipt/jipt.js'
end
end
+6
View File
@@ -54,6 +54,12 @@ module FrontendAssetHelper
end
end
def include_spot_assets
capture do
concat stylesheet_link_tag variable_asset_path("spot.css"), media: :all, skip_pipeline: true
end
end
private
def angular_cli_asset(path)
+76 -2
View File
@@ -29,8 +29,8 @@
module ProjectsHelper
include WorkPackagesFilterHelper
def filter_set?
params[:filters].present?
def show_filters_section?
params[:filters].present? && !params.key?(:hide_filters_section)
end
def allowed_filters(query)
@@ -46,6 +46,7 @@ module ProjectsHelper
Queries::Projects::Filters::TemplatedFilter,
Queries::Projects::Filters::PublicFilter,
Queries::Projects::Filters::ProjectStatusFilter,
Queries::Projects::Filters::MemberOfFilter,
Queries::Projects::Filters::CreatedAtFilter,
Queries::Projects::Filters::LatestActivityAtFilter,
Queries::Projects::Filters::NameAndIdentifierFilter,
@@ -64,6 +65,79 @@ module ProjectsHelper
end
end
def global_menu_items
[
global_menu_all_projects_item,
global_menu_my_projects_item,
global_menu_public_projects_item,
global_menu_archived_projects_item
]
end
def global_menu_all_projects_item
path = projects_path
[
t(:label_all_projects),
path,
{ class: global_menu_item_css_class(path),
title: t(:label_all_projects) }
]
end
def global_menu_my_projects_item
path = projects_path_with_filters(
[{ member_of: { operator: '=', values: ['t'] } }]
)
[
t(:label_my_projects),
path,
{ class: global_menu_item_css_class(path),
title: t(:label_my_projects) }
]
end
def global_menu_public_projects_item
path = projects_path_with_filters(
[{ public: { operator: '=', values: ['t'] } }]
)
[
t(:label_public_projects),
path,
{ class: global_menu_item_css_class(path),
title: t(:label_public_projects) }
]
end
def global_menu_archived_projects_item
path = projects_path_with_filters(
[{ active: { operator: '=', values: ['f'] } }]
)
[
t(:label_archived_projects),
path,
{ class: global_menu_item_css_class(path),
title: t(:label_archived_projects) }
]
end
def projects_path_with_filters(filters)
return projects_path if filters.empty?
projects_path(filters: filters.to_json, hide_filters_section: true)
end
def global_menu_item_css_class(path)
"op-sidemenu--item-action #{global_menu_item_selected(path) ? 'selected' : ''}"
end
def global_menu_item_selected(menu_item_path)
menu_item_path == request.fullpath
end
def project_more_menu_items(project)
[project_more_menu_subproject_item(project),
project_more_menu_settings_item(project),
+13 -1
View File
@@ -118,11 +118,23 @@ module UsersHelper
end
end
def visible_user_information?(user)
user.pref.can_expose_mail? || user.visible_custom_field_values.any? { _1.value.present? }
end
def user_name(user)
user ? user.name : I18n.t('user.deleted')
end
def allowed_management_user_profile_path(user)
if User.current.allowed_to_globally?(:manage_user)
edit_user_path(user)
else
user_path(user)
end
end
def can_users_have_auth_source?
AuthSource.any? && !OpenProject::Configuration.disable_password_login?
LdapAuthSource.any? && !OpenProject::Configuration.disable_password_login?
end
end
-106
View File
@@ -1,106 +0,0 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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 AuthSource < ApplicationRecord
include Redmine::Ciphering
class Error < ::StandardError; end
has_many :users
validates :name,
uniqueness: { case_sensitive: false },
length: { maximum: 60 }
def self.unique_attribute
:name
end
prepend ::Mixins::UniqueFinder
def authenticate(_login, _password); end
def find_user(_login)
raise "subclass repsonsiblity"
end
# implemented by a subclass, should raise when no connection is possible and not raise on success
def test_connection
raise I18n.t('auth_source.using_abstract_auth_source')
end
def auth_method_name
'Abstract'
end
def account_password
read_ciphered_attribute(:account_password)
end
def account_password=(arg)
write_ciphered_attribute(:account_password, arg)
end
def allow_password_changes?
self.class.allow_password_changes?
end
# Does this auth source backend allow password changes?
def self.allow_password_changes?
false
end
# Try to authenticate a user not yet registered against available sources
def self.authenticate(login, password)
AuthSource.where(['onthefly_register=?', true]).find_each do |source|
begin
Rails.logger.debug { "Authenticating '#{login}' against '#{source.name}'" }
attrs = source.authenticate(login, password)
rescue StandardError => e
Rails.logger.error "Error during authentication: #{e.message}"
attrs = nil
end
return attrs if attrs
end
nil
end
def self.find_user(login)
AuthSource.where(['onthefly_register=?', true]).find_each do |source|
begin
Rails.logger.debug { "Looking up '#{login}' in '#{source.name}'" }
attrs = source.find_user login
rescue StandardError => e
Rails.logger.error "Error during authentication: #{e.message}"
attrs = nil
end
return attrs if attrs
end
nil
end
end
+1 -1
View File
@@ -275,7 +275,7 @@ class CustomField < ApplicationRecord
end
def multi_value_possible?
%w[user list].include?(field_format) &&
%w[version user list].include?(field_format) &&
[ProjectCustomField, WorkPackageCustomField].include?(self.class)
end
-60
View File
@@ -1,60 +0,0 @@
class DummyAuthSource < AuthSource
def test_connection
# the dummy connection is always available
end
def authenticate(login, password)
existing_user(login, password) || on_the_fly_user(login)
end
def find_user(login)
find_registered_user(login) || find_on_the_fly_user(login)
end
def auth_method_name
'LDAP'
end
private
def find_registered_user(login)
registered_login?(login) &&
User
.find_by(login:)
.attributes
.slice("firstname", "lastname", "mail")
.merge(auth_source_id: id)
end
def find_on_the_fly_user(login)
dummy_login?(login) && on_the_fly_user(login)
end
def dummy_login?(login)
login.start_with? "dummy_"
end
def existing_user(login, password)
registered_login?(login) && password == 'dummy' && find_registered_user(login)
end
def on_the_fly_user(login)
return nil unless onthefly_register?
{
firstname: login.capitalize,
lastname: 'Dummy',
mail: 'login@DerpLAP.net',
auth_source_id: id
}
end
def registered_login?(login)
not users.where(login:).empty? # empty? to use EXISTS query
end
# Does this auth source backend allow password changes?
def self.allow_password_changes?
false
end
end
@@ -61,7 +61,7 @@ class Journable::HistoricActiveRecordRelation < ActiveRecord::Relation
instance_variable_set key, relation.instance_variable_get(key)
end
self.timestamp = timestamp
self.timestamp = Array(timestamp)
readonly!
instance_variable_set :@table, model.journal_class.arel_table
end
@@ -240,7 +240,12 @@ class Journable::HistoricActiveRecordRelation < ActiveRecord::Relation
#
def add_timestamp_condition(relation)
relation.joins_values = [journals_join_statement] + relation.joins_values
relation.merge(Journal.where(journable_type: model.name).at_timestamp(timestamp))
timestamp_condition = timestamp.map do |t|
Journal.where(journable_type: model.name).at_timestamp(t)
end.reduce(&:or)
relation.merge(timestamp_condition)
end
def journals_join_statement
@@ -267,7 +272,7 @@ class Journable::HistoricActiveRecordRelation < ActiveRecord::Relation
# Does not work yet for other includes.
#
def add_join_projects_on_work_package_journals(relation)
if include_projects?(relation)
if include_projects?
relation
.except(:includes, :eager_load, :preload)
.joins('LEFT OUTER JOIN "projects" ' \
@@ -277,8 +282,8 @@ class Journable::HistoricActiveRecordRelation < ActiveRecord::Relation
end
end
def include_projects?(relation)
include_values = relation.values.fetch(:includes, [])
def include_projects?
include_values = values.fetch(:includes, [])
include_values.include?(:project)
end
@@ -288,7 +293,7 @@ class Journable::HistoricActiveRecordRelation < ActiveRecord::Relation
# - the `work_package_journals` table (data)
# - the `journals` table
#
# Also, add the `timestamp` as column so that we have it as attribute in our model.
# Also, add the `timestamp` and `journal_id` as column so that we have it as attribute in our model.
#
def select_columns_from_the_appropriate_tables(relation)
if relation.select_values.count == 0
@@ -311,7 +316,8 @@ class Journable::HistoricActiveRecordRelation < ActiveRecord::Relation
"journals.journable_id as id",
"journables.created_at as created_at",
"journals.updated_at as updated_at",
"'#{timestamp}' as timestamp"
"CASE #{timestamp_case_when_statements} END as timestamp",
"journals.id as journal_id"
] + \
model.column_names_missing_in_journal.collect do |missing_column_name|
"null as #{missing_column_name}"
@@ -377,6 +383,23 @@ class Journable::HistoricActiveRecordRelation < ActiveRecord::Relation
sql_string.gsub! "\"#{CustomValue.table_name}\".", "\"#{Journal::CustomizableJournal.table_name}\"."
end
def timestamp_case_when_statements
timestamp
.map do |timestamp|
comparison_time = case timestamp
when Timestamp
timestamp.to_time
when DateTime
timestamp.in_time_zone
else
raise NotImplementedError, "Unknown timestamp type: #{timestamp.class}"
end
"WHEN \"journals\".\"validity_period\" @> timestamp with time zone '#{comparison_time}' THEN '#{timestamp}'"
end
.join(" ")
end
class NotImplementedError < StandardError; end
end
+19 -17
View File
@@ -84,27 +84,29 @@ module Journable::Timestamps
end
end
# Instantiates a journable with historic data from the given timestap.
# Instantiates a journable with historic data from the given timestamp.
#
# WorkPackage.find(1).at_timestamp(1.year.ago)
#
def at_timestamp(timestamp)
if journal = journals.at_timestamp(timestamp).first
attributes = journal.data.attributes.merge(
{
"id" => id,
"created_at" => created_at,
"updated_at" => journal.updated_at,
"timestamp" => timestamp
}
)
self.class.column_names_missing_in_journal.each do |missing_column_name|
attributes[missing_column_name] = nil
end
journable = self.class.instantiate(attributes)
journable.readonly!
journable
return unless journal = journals.at_timestamp(timestamp).first
attributes = journal.data.attributes.merge(
{
"id" => id,
"created_at" => created_at,
"updated_at" => journal.updated_at,
"timestamp" => timestamp,
"journal_id" => journal.id
}
)
self.class.column_names_missing_in_journal.each do |missing_column_name|
attributes[missing_column_name] = nil
end
journable = self.class.instantiate(attributes)
::Journable::WithHistoricAttributes.load_custom_values(journable)
journable.readonly!
journable
end
def historic?
@@ -122,6 +124,6 @@ module Journable::Timestamps
def rollback!
raise ActiveRecord::RecordNotSaved, "This is no historic data. You can only revert to historic data." unless historic?
self.class.find(id).update! attributes.except("id", "timestamp")
self.class.find(id).update! attributes.except("id", "timestamp", "journal_id", "created_at", "updated_at")
end
end
@@ -118,6 +118,10 @@ class Journable::WithHistoricAttributes < SimpleDelegator
end
end
def load_custom_values(journalized)
Loader.new(journalized).load_custom_values
end
private
def wrap_each_journable(journables, timestamps:, query:, include_only_changed_attributes:)
@@ -133,6 +137,10 @@ class Journable::WithHistoricAttributes < SimpleDelegator
end
end
# The `attributes_by_timestamp` method is not being directly used in the api to render the
# attributesByTimestamp object inside the historic work packages.
# It serves as a console tool at the moment.
def attributes_by_timestamp
@attributes_by_timestamp ||= Hash.new do |h, t|
attributes = if include_only_changed_attributes
@@ -227,9 +235,29 @@ class Journable::WithHistoricAttributes < SimpleDelegator
historic_journable = at_timestamp(Timestamp.parse(timestamp))
return unless historic_journable
changes = ::Acts::Journalized::JournableDiffer.changes(__getobj__, historic_journable)
::Acts::Journalized::JournableDiffer
.changes(__getobj__, historic_journable)
# In the other occurrences of JournableDiffer.association_changes calls, we are using the plural
# of the association name (`custom_fields` in this instance), to map the association fields. That
# will result in a changes hash containing { "custom_fields_1" => ... }. This makes sense in the case
# of journal changes, because the formatted fields have the convention for plural lookup for journals
# defined in the `register_journal_formatted_fields(:custom_field, /custom_fields_\d+/)`.
# In this case the diff is part of the WorkPackageAtTimestampRepresenter where the `representable_map`
# contains the singular names (`custom_field_1`), hence we need to map the diffs to match that format.
# As a food for thought, I think it would be more handy to use the singular naming everywhere.
changes.merge!(
::Acts::Journalized::JournableDiffer.association_changes(
historic_journable,
__getobj__,
'custom_values',
'custom_field',
:custom_field_id,
:value
)
)
changes
end
def changed_attributes_at_timestamp(timestamp)
@@ -38,7 +38,7 @@ class Journable::WithHistoricAttributes
def at_timestamp(timestamp)
@at_timestamp ||= Hash.new do |h, t|
h[t] = (currently_invisible_journalized_at_timestamp(t) + currently_visible_journalized_at_timestamp(t)).index_by(&:id)
h[t] = journalized_at_timestamp(t).index_by(&:id)
end
@at_timestamp[timestamp]
@@ -54,6 +54,25 @@ class Journable::WithHistoricAttributes
@work_package_ids_of_query_at_timestamp[query][timestamp]
end
def load_custom_values(journalized = journables)
journal_ids = begin
journalized.map(&:journal_id)
rescue NoMethodError
raise ArgumentError,
'The provided journalized items do not have a journal_id included. ' \
'Please load them via any of the Journable::Timestamps#at_timestamp method. ' \
'ie: WorkPackage.at_timestamp(1.day.ago) or WorkPackage.find(1).at_timestamp(1.day.ago)'
end
customizable_journals_by_journal_id = load_customizable_journals_by_journal_id(journal_ids)
journalized.each do |work_package|
customizable_journals = Array(customizable_journals_by_journal_id[work_package.journal_id])
set_custom_value_association_from_journal!(work_package:, customizable_journals:)
end
journalized
end
private
def work_package_ids_of_query_at_timestamp_calculation(query, timestamp)
@@ -74,6 +93,11 @@ class Journable::WithHistoricAttributes
@currently_invisible_journables ||= journables - currently_visible_journables
end
def journalized_at_timestamp(tms)
journalized = (currently_invisible_journalized_at_timestamp(tms) + currently_visible_journalized_at_timestamp(tms))
load_custom_values(journalized)
end
def currently_invisible_journalized_at_timestamp(timestamp)
journalized_class.visible.at_timestamp(timestamp).where(id: currently_invisible_journables)
end
@@ -86,6 +110,25 @@ class Journable::WithHistoricAttributes
journables.first.class
end
def load_customizable_journals_by_journal_id(journal_ids)
Journal::CustomizableJournal
.where(journal_id: journal_ids)
.includes(:custom_field)
.group_by(&:journal_id)
end
def set_custom_value_association_from_journal!(work_package:, customizable_journals:)
# Build the associated customizable_journals as custom values, this way the historic work packages
# will behave just as the normal ones. Additionally set the reverse customized association
# on the custom_values that points to the work_package itself.
historic_custom_values = customizable_journals.map do |customizable_journal|
customizable_journal.as_custom_value(customized: work_package)
end
work_package.association(:custom_values).loaded!
work_package.association(:custom_values).target = historic_custom_values
end
attr_accessor :journables
end
private_constant :Loader
+7
View File
@@ -51,6 +51,7 @@ class Journal < ApplicationRecord
register_journal_formatter :wiki_diff, OpenProject::JournalFormatter::WikiDiff
register_journal_formatter :time_entry_named_association, OpenProject::JournalFormatter::TimeEntryNamedAssociation
register_journal_formatter :cause, OpenProject::JournalFormatter::Cause
register_journal_formatter :file_link, OpenProject::JournalFormatter::FileLink
# Attributes related to the cause are stored in a JSONB column so we can easily add new relations and related
# attributes without a heavy database migration. Fields will be prefixed with `cause_` but are stored in the JSONB
@@ -62,6 +63,7 @@ class Journal < ApplicationRecord
work_package_children_changed_times
work_package_related_changed_times
working_days_changed
system_update
].freeze
# Make sure each journaled model instance only has unique version ids
@@ -74,6 +76,7 @@ class Journal < ApplicationRecord
has_many :attachable_journals, class_name: 'Journal::AttachableJournal', dependent: :delete_all
has_many :customizable_journals, class_name: 'Journal::CustomizableJournal', dependent: :delete_all
has_many :storable_journals, class_name: 'Journal::StorableJournal', dependent: :delete_all
has_many :notifications, dependent: :destroy
@@ -149,6 +152,10 @@ class Journal < ApplicationRecord
private
def has_file_links?
journable.respond_to?(:file_links)
end
def predecessor
@predecessor ||= if initial?
nil
@@ -30,4 +30,10 @@ class Journal::CustomizableJournal < Journal::AssociatedJournal
self.table_name = 'customizable_journals'
belongs_to :custom_field
def as_custom_value(attributes = {})
custom_value = CustomValue.new(attributes.merge(custom_field:, value:))
custom_value.readonly!
custom_value
end
end
+35
View File
@@ -0,0 +1,35 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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 Journal::StorableJournal < Journal::AssociatedJournal
self.table_name = 'storages_file_links_journals'
belongs_to :file_link, class_name: 'Storages::FileLink'
end
+1 -3
View File
@@ -70,9 +70,7 @@ module Journal::Timestamps
timestamp = timestamp.to_time if timestamp.kind_of? Timestamp
timestamp = timestamp.in_time_zone if timestamp.kind_of? DateTime
where(id: where(created_at: (..timestamp)) \
.select("distinct on (journable_type, journable_id) id") \
.reorder(:journable_type, :journable_id, created_at: :desc))
where(['validity_period @> timestamp with time zone ?', timestamp])
end
end
end
+66 -10
View File
@@ -28,7 +28,23 @@
require 'net/ldap'
class LdapAuthSource < AuthSource
class LdapAuthSource < ApplicationRecord
class Error < ::StandardError; end
include Redmine::Ciphering
has_many :users,
dependent: :nullify
validates :name,
uniqueness: { case_sensitive: false },
length: { maximum: 60 }
def self.unique_attribute
:name
end
prepend ::Mixins::UniqueFinder
enum tls_mode: {
plain_ldap: 0,
simple_tls: 1,
@@ -48,6 +64,48 @@ class LdapAuthSource < AuthSource
after_initialize :set_default_port
before_validation :strip_ldap_attributes
# Try to authenticate a user not yet registered against available sources
def self.authenticate(login, password)
where(onthefly_register: true).find_each do |source|
begin
Rails.logger.debug { "Authenticating '#{login}' against '#{source.name}'" }
attrs = source.authenticate(login, password)
rescue StandardError => e
Rails.logger.error "Error during authentication: #{e.message}"
attrs = nil
end
return attrs if attrs
end
nil
end
def self.find_user(login)
where(onthefly_register: true).find_each do |source|
begin
Rails.logger.debug { "Looking up '#{login}' in '#{source.name}'" }
attrs = source.find_user login
rescue StandardError => e
Rails.logger.error "Error during authentication: #{e.message}"
attrs = nil
end
return attrs if attrs
end
nil
end
def seeded_from_env?
Setting.seed_ldap&.key?(name)
end
def account_password
read_ciphered_attribute(:account_password)
end
def account_password=(arg)
write_ciphered_attribute(:account_password, arg)
end
def authenticate(login, password)
return nil if login.blank? || password.blank?
@@ -58,7 +116,7 @@ class LdapAuthSource < AuthSource
attrs.except(:dn)
end
rescue Net::LDAP::Error => e
raise AuthSource::Error, "LdapError: #{e.message}"
raise LdapAuthSource::Error, "LdapError: #{e.message}"
end
def find_user(login)
@@ -71,7 +129,7 @@ class LdapAuthSource < AuthSource
attrs.except(:dn)
end
rescue Net::LDAP::Error => e
raise AuthSource::Error, "LdapError: #{e.message}"
raise LdapAuthSource::Error, "LdapError: #{e.message}"
end
# Open and return a system connection
@@ -82,20 +140,18 @@ class LdapAuthSource < AuthSource
# test the connection to the LDAP
def test_connection
unless authenticate_dn(account, account_password)
raise AuthSource::Error, I18n.t('auth_source.ldap_error', error_message: I18n.t('auth_source.ldap_auth_failed'))
raise LdapAuthSource::Error,
I18n.t('ldap_auth_sources.ldap_error', error_message: I18n.t('ldap_auth_sources.ldap_auth_failed'))
end
rescue Net::LDAP::Error => e
raise AuthSource::Error, I18n.t('auth_source.ldap_error', error_message: e.to_s)
end
def auth_method_name
'LDAP'
raise LdapAuthSource::Error,
I18n.t('ldap_auth_sources.ldap_error', error_message: e.to_s)
end
def get_user_attributes_from_ldap_entry(entry)
base_attributes = {
dn: entry.dn,
auth_source_id: id
ldap_auth_source_id: id
}
base_attributes.merge mapped_attributes(entry)
+4 -4
View File
@@ -62,8 +62,8 @@ class PermittedParams
params.require(:attribute_help_text).permit(*self.class.permitted_attributes[:attribute_help_text])
end
def auth_source
params.require(:auth_source).permit(*self.class.permitted_attributes[:auth_source])
def ldap_auth_source
params.require(:ldap_auth_source).permit(*self.class.permitted_attributes[:ldap_auth_source])
end
def forum
@@ -210,7 +210,7 @@ class PermittedParams
change_password_allowed,
additional_params = [])
additional_params << :auth_source_id unless external_authentication
additional_params << :ldap_auth_source_id unless external_authentication
if current_user.admin?
additional_params << :force_password_change if change_password_allowed
@@ -416,7 +416,7 @@ class PermittedParams
attribute_name
help_text
),
auth_source: %i(
ldap_auth_source: %i(
name
host
port
+4 -8
View File
@@ -85,8 +85,8 @@ class Project < ApplicationRecord
association_foreign_key: 'custom_field_id'
has_many :budgets, dependent: :destroy
has_many :notification_settings, dependent: :destroy
has_many :projects_storages, dependent: :destroy, class_name: 'Storages::ProjectStorage'
has_many :storages, through: :projects_storages
has_many :project_storages, dependent: :destroy, class_name: 'Storages::ProjectStorage'
has_many :storages, through: :project_storages
acts_as_customizable
acts_as_searchable columns: %W(#{table_name}.name #{table_name}.identifier #{table_name}.description),
@@ -157,6 +157,8 @@ class Project < ApplicationRecord
scope :visible, ->(user = User.current) { where(id: Project.visible_by(user)) }
scope :newest, -> { order(created_at: :desc) }
scope :active, -> { where(active: true) }
scope :with_member, ->(user = User.current) { where(id: user.memberships.select(:project_id)) }
scope :without_member, ->(user = User.current) { where.not(id: user.memberships.select(:project_id)) }
scopes :activated_time_activity,
:visible_with_activated_time_activity
@@ -211,12 +213,6 @@ class Project < ApplicationRecord
Authorization.projects(permission, user)
end
def reload(*args)
@all_work_package_custom_fields = nil
super
end
# Returns a :conditions SQL string that can be used to find the issues associated with this project.
#
# Examples:
+1
View File
@@ -35,6 +35,7 @@ module Queries::Projects
filter Filters::PublicFilter
filter Filters::NameFilter
filter Filters::NameAndIdentifierFilter
filter Filters::MemberOfFilter
filter Filters::TypeaheadFilter
filter Filters::CustomFieldFilter
filter Filters::CreatedAtFilter
@@ -0,0 +1,54 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2023 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::Projects::Filters::MemberOfFilter < Queries::Projects::Filters::ProjectFilter
include Queries::Filters::Shared::BooleanFilter
def self.key
:member_of
end
def scope
if allowed_values.first.intersect?(values)
model.visible.with_member
else
model.visible.without_member
end
end
def available_operators
[::Queries::Operators::BooleanEquals]
end
def human_name
I18n.t(:label_i_am_member)
end
end
@@ -72,23 +72,21 @@ class Queries::WorkPackages::Filter::SearchFilter <
end
def custom_field_configurations
custom_fields =
if context&.project
context.project.all_work_package_custom_fields.select do |custom_field|
%w(text string).include?(custom_field.field_format) &&
custom_field.is_filter == true &&
custom_field.searchable == true
end
else
::WorkPackageCustomField
.filter
.for_all
.where(field_format: %w(text string),
is_filter: true,
searchable: true)
end
# Does not remove custom fields that are not marked as filters
# as the intend of this filter is to search and it is used in the
# search context. Thus, only the searchable flag is of interest.
custom_fields = if context&.project
context
.project
.all_work_package_custom_fields
else
::WorkPackageCustomField
end
custom_fields.map do |custom_field|
custom_fields
.where(field_format: %w(text string),
searchable: true)
.map do |custom_field|
Queries::WorkPackages::Filter::FilterConfiguration.new(
Queries::WorkPackages::Filter::CustomFieldFilter,
custom_field.column_name,
+1 -2
View File
@@ -437,8 +437,7 @@ class Query < ApplicationRecord
end
def allowed_timestamps
return timestamps if EnterpriseToken.allows_to?(:baseline_comparison)
timestamps.select { |t| t.one_day_ago? || t.to_time >= Date.yesterday }
Timestamp.allowed(timestamps)
end
def valid_filter_subset!
+4 -15
View File
@@ -51,12 +51,7 @@ class ::Query::Results
def sorted_work_packages_matching_the_filters_today
sorted_work_packages
.visible
.merge(filtered_work_packages_matching_the_filters_today)
end
def sorted_work_packages_matching_the_filters_at_any_of_the_given_timestamps
sorted_work_packages
.where(id: work_packages_matching_the_filters_at_any_of_the_given_timestamps)
.merge(filtered_work_packages.merge(filter_merges))
end
# For filtering on historic data, this returns the work packages
@@ -65,11 +60,9 @@ class ::Query::Results
# concatenation that means that a user has to have no permission to see a work package
# at any of the timestamps. This has to be used with care. Callers will have to
# ensure to not reveal information.
def work_packages_matching_the_filters_at_any_of_the_given_timestamps
query.timestamps.collect do |timestamp|
WorkPackage
.where(id: filtered_work_packages.visible.at_timestamp(timestamp))
end.reduce(:or)
def sorted_work_packages_matching_the_filters_at_any_of_the_given_timestamps
sorted_work_packages
.where(id: filtered_work_packages.visible.at_timestamp(query.timestamps))
end
# Returns an active-record relation that applies the filters to find the matching
@@ -86,10 +79,6 @@ class ::Query::Results
.where(query.statement)
end
def filtered_work_packages_matching_the_filters_today
filtered_work_packages.merge(filter_merges)
end
def sorted_work_packages
work_package_scope
.joins(sort_criteria_joins)
+12 -7
View File
@@ -39,7 +39,13 @@ class Role < ApplicationRecord
where("#{compare} builtin = #{NON_BUILTIN}")
}
before_destroy :check_deletable
before_destroy(prepend: true) do
unless deletable?
errors.add(:base, "can't be destroyed")
raise ActiveRecord::RecordNotDestroyed
end
end
has_many :workflows, dependent: :delete_all do
def copy_from_role(source_role)
Workflow.copy(nil, source_role, nil, proxy_association.owner)
@@ -74,7 +80,7 @@ class Role < ApplicationRecord
end
def permissions=(perms)
not_included_yet = (perms.map(&:to_sym) - permissions).reject(&:blank?)
not_included_yet = (perms.map(&:to_sym) - permissions).compact_blank
included_until_now = permissions - perms.map(&:to_sym)
remove_permission!(*included_until_now)
@@ -176,6 +182,10 @@ class Role < ApplicationRecord
.first
end
def deletable?
members.none? && !builtin?
end
private
def allowed_permissions
@@ -188,11 +198,6 @@ class Role < ApplicationRecord
end
end
def check_deletable
raise "Can't delete role" if members.any?
raise "Can't delete builtin role" if builtin?
end
def add_permission(permission)
if persisted?
role_permissions.create(permission:)
+6
View File
@@ -102,6 +102,12 @@ class Timestamp
def now
new(ActiveSupport::Duration.build(0).iso8601)
end
def allowed(timestamps)
return timestamps if EnterpriseToken.allows_to?(:baseline_comparison)
timestamps.select { |t| t.one_day_ago? || t.to_time >= Date.yesterday }
end
end
def initialize(arg = Timestamp.now.to_s)
+8 -9
View File
@@ -61,7 +61,7 @@ class User < Principal
# in order to keep all previously generated ical urls valid and usable
has_many :ical_tokens, class_name: '::Token::ICal', dependent: :destroy
belongs_to :auth_source, optional: true
belongs_to :ldap_auth_source, optional: true
# Authorized OAuth grants
has_many :oauth_grants,
@@ -157,7 +157,7 @@ class User < Principal
# create new password if password was set
def update_password
if password && auth_source_id.blank?
if password && ldap_auth_source_id.blank?
new_password = passwords.build(type: UserPassword.active_type.to_s)
new_password.plain_password = password
new_password.save
@@ -211,9 +211,9 @@ class User < Principal
return nil if !user.active? || OpenProject::Configuration.disable_password_login?
if user.auth_source
if user.ldap_auth_source
# user has an external authentication method
return nil unless user.auth_source.authenticate(user.login, password)
return nil unless user.ldap_auth_source.authenticate(user.login, password)
else
# authentication with local password
return nil unless user.check_password?(password)
@@ -240,7 +240,7 @@ class User < Principal
def self.try_authentication_and_create_user(login, password)
return nil if OpenProject::Configuration.disable_password_login?
attrs = AuthSource.authenticate(login, password)
attrs = LdapAuthSource.authenticate(login, password)
return unless attrs
call = Users::CreateService
@@ -326,8 +326,8 @@ class User < Principal
# If +update_legacy+ is set, will automatically save legacy passwords using the current
# format.
def check_password?(clear_password, update_legacy: true)
if auth_source_id.present?
auth_source.authenticate(login, clear_password)
if ldap_auth_source.present?
ldap_auth_source.authenticate(login, clear_password)
else
return false if current_password.nil?
@@ -339,9 +339,8 @@ class User < Principal
def change_password_allowed?
return false if uses_external_authentication? ||
OpenProject::Configuration.disable_password_login?
return true if auth_source_id.blank?
auth_source.allow_password_changes?
ldap_auth_source_id.blank?
end
# Is the user authenticated via an external authentication source via OmniAuth?
+8
View File
@@ -94,6 +94,10 @@ class UserPreference < ApplicationRecord
settings.fetch(:hide_mail, true)
end
def can_expose_mail?
!hide_mail
end
def auto_hide_popups=(value)
settings[:auto_hide_popups] = to_boolean(value)
end
@@ -119,6 +123,10 @@ class UserPreference < ApplicationRecord
settings[:comments_sorting] = to_boolean(value) ? 'desc' : 'asc'
end
def theme
super.presence || Setting.user_default_theme
end
def time_zone
super.presence || Setting.user_default_timezone.presence
end
+11 -8
View File
@@ -81,10 +81,11 @@ class Version < ApplicationRecord
# Returns the total reported time for this version
def spent_hours
@spent_hours ||= TimeEntry
.includes(:work_package)
.where(work_packages: { version_id: id })
.sum(:hours)
.to_f
.not_ongoing
.includes(:work_package)
.where(work_packages: { version_id: id })
.sum(:hours)
.to_f
end
def closed?
@@ -148,7 +149,9 @@ class Version < ApplicationRecord
@wiki_page
end
def to_s; name end
def to_s
name
end
def to_s_with_project
"#{project} - #{name}"
@@ -212,9 +215,9 @@ class Version < ApplicationRecord
)
done = work_packages
.where(statuses: { is_closed: !open })
.includes(:status)
.sum(sum_sql)
.where(statuses: { is_closed: !open })
.includes(:status)
.sum(sum_sql)
progress = done.to_f / (estimated_average * issues_count)
end
progress
+12
View File
@@ -93,6 +93,18 @@ class WikiPage < ApplicationRecord
title.to_localized_slug(locale: :en)
end
# Using a hook will not allow us to retry the call
# rubocop:disable Rails/ActiveRecordOverride
def destroy
super
rescue ActiveRecord::StaleObjectError
# Due to closure_tree, parent_id might have changed
# when destroying all wiki pages
reload
super
end
# rubocop:enable Rails/ActiveRecordOverride
def delete_wiki_menu_item
menu_item&.destroy
# ensure there is a menu item for the wiki
+39 -1
View File
@@ -449,9 +449,47 @@ class WorkPackage < ApplicationRecord
# Overrides Redmine::Acts::Customizable::ClassMethods#available_custom_fields
def self.available_custom_fields(work_package)
WorkPackage::AvailableCustomFields.for(work_package.project, work_package.type)
if work_package.project_id && work_package.type_id
RequestStore.fetch(available_custom_field_key(work_package)) do
available_custom_fields_from_db([work_package])
end
else
[]
end
end
def self.preload_available_custom_fields(work_packages)
custom_fields = available_custom_fields_from_db(work_packages)
.select('projects.id project_id',
'types.id type_id',
'custom_fields.*')
work_packages.each do |work_package|
RequestStore.store[available_custom_field_key(work_package)] = custom_fields
.select do |cf|
((cf.project_id == work_package.project_id) || cf.is_for_all?) && cf.type_id == work_package.type_id
end
end
end
def self.available_custom_fields_from_db(work_packages)
WorkPackageCustomField
.left_joins(:projects, :types)
.where(projects: { id: work_packages.map(&:project_id).uniq },
types: { id: work_packages.map(&:type_id).uniq })
.or(WorkPackageCustomField
.left_joins(:projects, :types)
.references(:projects, :types)
.where(is_for_all: true)
.where(types: { id: work_packages.map(&:type_id).uniq }))
end
private_class_method :available_custom_fields_from_db
def self.available_custom_field_key(work_package)
:"#work_package_custom_fields_#{work_package.project_id}_#{work_package.type_id}"
end
private_class_method :available_custom_field_key
def custom_field_cache_key
[project_id, type_id]
end
+1
View File
@@ -86,6 +86,7 @@ module WorkPackage::Journalized
register_journal_formatted_fields(:custom_field, /custom_fields_\d+/)
register_journal_formatted_fields(:ignore_non_working_days, 'ignore_non_working_days')
register_journal_formatted_fields(:cause, 'cause')
register_journal_formatted_fields(:file_link, /file_links_?\d+/)
# Joined
register_journal_formatted_fields :named_association, :parent_id, :project_id,
@@ -37,7 +37,7 @@ module WorkPackage::PDFExport::Attachments
resized_file_path = tmp_file.path
image = MiniMagick::Image.open(file_path)
image.resize("x325>")
image.resize("x800>")
image.write(resized_file_path)
resized_file_path
+83 -28
View File
@@ -125,45 +125,84 @@ module WorkPackage::PDFExport::Common
"<link anchor=\"#{anchor}\">#{caption}</link>"
end
def align_to_left_position(text, align, text_style)
text_width = pdf.width_of(text, text_style)
if align == :right
pdf.bounds.right - text_width
elsif align == :center
(pdf.bounds.width - text_width) / 2
else
pdf.bounds.left
end
end
def link_target_at_current_y(id)
pdf_dest = pdf.dest_xyz(0, pdf.y)
pdf.add_dest(id.to_s, pdf_dest)
end
def draw_repeating_text(text:, align:, top:, text_style:)
left = align_to_left_position(text, align, text_style)
opts = text_style.merge({ at: [left, top] })
pdf.repeat :all do
pdf.draw_text text, opts
end
end
def draw_repeating_dynamic_text(align, top, text_style)
pdf.repeat :all, dynamic: true do
text = yield
left = align_to_left_position(text, align, text_style)
opts = text_style.merge({ at: [left, top] })
pdf.draw_text text, opts
end
end
def pdf_table_auto_widths(data, column_widths, options, &)
pdf.table(data, options.merge({ width: pdf.bounds.width }), &)
rescue Prawn::Errors::CannotFit
pdf.table(data, options.merge({ column_widths: }), &)
end
def draw_text_multiline_left(text:, text_style:, max_left:, top:, max_lines:)
lines = wrap_to_lines(text, max_left - pdf.bounds.left, text_style, max_lines)
starting_position = top
lines.reverse.each do |line|
starting_position += draw_text_multiline_part(line, text_style, pdf.bounds.left, starting_position)
end
end
def draw_text_multiline_right(text:, text_style:, max_left:, top:, max_lines:)
lines = wrap_to_lines(text, pdf.bounds.right - max_left, text_style, max_lines)
starting_position = top
lines.reverse.each do |line|
line_width = measure_text_width(line, text_style)
line_x = pdf.bounds.right - line_width
starting_position += draw_text_multiline_part(line, text_style, line_x, starting_position)
end
end
def draw_text_centered(text, text_style, top)
text_width = measure_text_width(text, text_style)
text_x = (pdf.bounds.width - text_width) / 2
pdf.draw_text text, text_style.merge({ at: [text_x, top] })
[text_x, text_width]
end
def draw_text_multiline_part(line, text_style, x_position, y_position)
pdf.draw_text line, text_style.merge({ at: [x_position, y_position] })
measure_text_height(line, text_style)
end
def truncate_ellipsis(text, available_width, text_style)
line = text.dup
while line.present? && (measure_text_width("#{line}...", text_style) > available_width)
line = line.chop
end
"#{line}..."
end
def split_wrapped_lines(text, available_width, text_style)
split_text = text.dup
lines = []
arranger = Prawn::Text::Formatted::Arranger.new(pdf)
line_wrapper = Prawn::Text::Formatted::LineWrap.new
until split_text.blank?
arranger.format_array = [text_style.merge({ text: split_text })]
single_line = line_wrapper.wrap_line(arranger:, width: available_width, document: pdf)
lines << single_line
split_text.slice!(single_line)
end
lines
end
def wrap_to_lines(text, available_width, text_style, max_lines)
split_text = text.dup
title_text_width = measure_text_width(split_text, text_style)
if title_text_width < available_width
[split_text]
else
lines = split_wrapped_lines(text, available_width, text_style)
if lines.length > max_lines
lines[max_lines - 1] = truncate_ellipsis(lines[max_lines - 1], available_width, text_style)
lines = lines.first(3)
end
lines
end
end
def measure_text_width(text, opts)
@pdf.save_font do
@pdf.font(opts[:font], opts)
@@ -171,6 +210,13 @@ module WorkPackage::PDFExport::Common
end
end
def measure_text_height(text, opts)
@pdf.save_font do
@pdf.font(opts[:font], opts)
@pdf.height_of(text, opts)
end
end
def text_column?(column)
column.is_a?(Queries::WorkPackages::Columns::CustomFieldColumn) &&
%w(string text).include?(column.custom_field.field_format)
@@ -215,6 +261,15 @@ module WorkPackage::PDFExport::Common
options[:show_images]
end
def build_pdf_filename(base)
suffix = "_#{title_datetime}.pdf"
"#{truncate(base, length: 255 - suffix.chars.length)}#{suffix}".gsub(' ', '-')
end
def title_datetime
DateTime.now.strftime('%Y-%m-%d_%H-%M')
end
def current_page_nr
pdf.page_number + @page_count
end
+11 -19
View File
@@ -75,27 +75,19 @@ module WorkPackage::PDFExport::Markdown
end
def handle_unknown_inline_html_tag(tag, node, opts)
result = []
case tag.name
when 'mention'
return [handle_mention_html_tag(tag, node, opts), opts]
when 'span'
text = tag.text
result.push(text_hash(text, opts)) if text.present?
else
result.push(text_hash(tag.to_s, opts))
end
result = 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
data_inlinehtml_tag(tag, node, opts)
end
[result, opts]
end
def handle_unknown_html_tag(tag, node, opts)
case tag.name
when 'figure', 'div', 'p', 'figcaption'
# nop, but scan children [true, ...]
else
draw_formatted_text([text_hash(tag.to_s, opts)], opts, node)
return [false, opts]
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]
end
@@ -107,7 +99,7 @@ module WorkPackage::PDFExport::Markdown
def write_markdown!(work_package, markdown)
md2pdf = MD2PDF.new(styles.wp_markdown_styling_yml)
md2pdf.draw_markdown(markdown, pdf, ->(src) {
with_attachments? ? attachment_image_filepath(work_package, src) : nil
with_images? ? attachment_image_filepath(work_package, src) : nil
})
end
@@ -32,8 +32,12 @@ module WorkPackage::PDFExport::MarkdownField
def write_markdown_field!(work_package, markdown, label)
return if markdown.blank?
write_optional_page_break
with_margin(styles.wp_markdown_label_margins) do
pdf.formatted_text([styles.wp_markdown_label.merge({ text: label })])
end
with_margin(styles.wp_markdown_margins) do
write_markdown! work_package, "# <font size=\"#{styles.wp_markdown_label_size}\">#{label}</font>\n\n#{markdown}"
write_markdown! work_package, markdown
end
end
end
+26 -25
View File
@@ -27,6 +27,8 @@
#++
module WorkPackage::PDFExport::Page
MAX_NR_OF_PDF_FOOTER_LINES = 3
def configure_page_size!(layout)
pdf.options[:page_layout] = layout
pdf.options[:page_size] = styles.page_size
@@ -91,36 +93,35 @@ module WorkPackage::PDFExport::Page
end
def write_footers!
write_footer_date!
write_footer_page_nr!
write_footer_title!
pdf.repeat :all, dynamic: true do
draw_footer_on_page
end
end
def write_footer_page_nr!
draw_repeating_dynamic_text(:center, styles.page_footer_offset, styles.page_footer) do
current_page_nr.to_s + total_page_nr_text
end
def draw_footer_on_page
top = styles.page_footer_offset
text_style = styles.page_footer
spacing = styles.page_footer_horizontal_spacing
page_nr_x, page_nr_width = draw_text_centered(footer_page_nr, text_style, top)
draw_text_multiline_left(
text: footer_date, max_left: page_nr_x - spacing,
text_style:, top:, max_lines: MAX_NR_OF_PDF_FOOTER_LINES
)
draw_text_multiline_right(
text: footer_title, max_left: page_nr_x + page_nr_width + spacing,
text_style:, top:, max_lines: MAX_NR_OF_PDF_FOOTER_LINES
)
end
def footer_page_nr
current_page_nr.to_s + total_page_nr_text
end
def footer_date
format_time(Time.zone.now, true)
end
def total_page_nr_text
@total_page_nr ? "/#{@total_page_nr}" : ''
end
def write_footer_title!
draw_repeating_text(
text: heading,
align: :right,
top: styles.page_footer_offset,
text_style: styles.page_footer
)
end
def write_footer_date!
draw_repeating_text(
text: format_time(Time.zone.now, true),
align: :left,
top: styles.page_footer_offset,
text_style: styles.page_footer
)
end
end
File diff suppressed because it is too large Load Diff
+23 -16
View File
@@ -14,6 +14,7 @@ page_header:
page_footer:
offset: -30
size: 8
spacing: 6
page_logo:
height: 20
@@ -49,6 +50,7 @@ overview:
toc:
subject_indent: 4
indent_mode: "stairs"
margin_top: 10
margin_bottom: 20
item:
@@ -66,10 +68,19 @@ toc:
work_package:
margin_bottom: 20
subject:
size: 14
size: 10
styles: [ "bold" ]
margin_bottom: 10
label:
subject_level_1:
size: 14
styles: [ "bold" ]
subject_level_2:
size: 13
styles: [ "bold" ]
subject_level_3:
size: 12
styles: [ "bold" ]
subject_level_4:
size: 11
styles: [ "bold" ]
attributes_table:
@@ -85,21 +96,13 @@ work_package:
border_width: 0.25
cell_label:
styles: [ "bold" ]
section:
heading:
size: 9
styles: [ "bold" ]
margin_top: 4
margin_bottom: 4
item:
size: 9
styles: [ "italic" ]
margin_left: 10
markdown_label:
size: 12
styles: [ "bold" ]
margin_top: 2
margin_bottom: 4
markdown_margin:
margin_top: 12
margin_bottom: 8
margin_bottom: 16
markdown:
font:
size: 10
@@ -107,9 +110,9 @@ work_package:
header:
size: 8
styles: [ "bold" ]
padding_top: 4
header_1:
size: 10
padding_bottom: 4
header_2:
size: 10
header_3:
@@ -153,7 +156,11 @@ work_package:
no_border_bottom: true
no_border_top: true
image:
align: "left"
align: "center"
margin_bottom: 4
caption:
size: 8
align: "center"
codeblock:
background_color: "F5F5F5"
color: "880000"
+19 -29
View File
@@ -49,6 +49,10 @@ module WorkPackage::PDFExport::Style
resolve_pt(@styles.dig(:page_footer, :offset), -30)
end
def page_footer_horizontal_spacing
resolve_pt(@styles.dig(:page_footer, :spacing), 6)
end
def page_logo_height
resolve_pt(@styles.dig(:page_logo, :height), 20)
end
@@ -133,6 +137,10 @@ module WorkPackage::PDFExport::Style
resolve_margin(@styles[:toc])
end
def toc_indent_mode
@styles.dig(:toc, :indent_mode)
end
def toc_item(level)
resolve_font(@styles.dig(:toc, :item)).merge(
resolve_font(@styles.dig(:toc, "item_level_#{level}".to_sym))
@@ -153,32 +161,10 @@ module WorkPackage::PDFExport::Style
resolve_margin(@styles[:work_package])
end
def wp_section_heading
resolve_font(@styles.dig(:work_package, :section, :heading))
end
def wp_section_heading_margins
resolve_margin(@styles.dig(:work_package, :section, :heading))
end
def wp_section_item
resolve_font(@styles.dig(:work_package, :section, :item))
end
def wp_section_item_margins
resolve_margin(@styles.dig(:work_package, :section, :item))
end
def wp_label
resolve_font(@styles.dig(:work_package, :label))
end
def wp_label_margins
resolve_margin(@styles.dig(:work_package, :label))
end
def wp_subject
resolve_font(@styles.dig(:work_package, :subject))
def wp_subject(level)
resolve_font(@styles.dig(:work_package, :subject)).merge(
resolve_font(@styles.dig(:work_package, "subject_level_#{level}".to_sym))
)
end
def wp_detail_subject_margins
@@ -199,12 +185,16 @@ module WorkPackage::PDFExport::Style
)
end
def wp_markdown_label_size
resolve_pt(resolve_font(@styles.dig(:work_package, :markdown_label))[:size], 12)
def wp_markdown_label
resolve_font(@styles.dig(:work_package, :markdown_label))
end
def wp_markdown_label_margins
resolve_margin(@styles.dig(:work_package, :markdown_label))
end
def wp_markdown_margins
resolve_margin(@styles.dig(:work_package, :markdown))
resolve_margin(@styles.dig(:work_package, :markdown_margin))
end
def wp_markdown_styling_yml
@@ -62,38 +62,97 @@ module WorkPackage::PDFExport::TableOfContents
measure_text_width(part, part_style) + styles.toc_item_subject_indent
end
def write_part_float(part, part_style)
def write_part_float(indent, part, part_style)
pdf.float do
pdf.text(part, part_style)
end
end
def write_toc!(toc_list)
max_level_string_width = toc_list.pluck(:level_string_width).max
toc_list.each do |toc_item|
with_margin(styles.toc_item_margins(toc_item[:level])) do
write_toc_item! toc_item, max_level_string_width
pdf.indent(indent) do
pdf.text(part, part_style)
end
end
end
def write_toc_item_subject!(toc_item, max_level_width, subject_style)
pdf.indent(max_level_width, toc_item[:page_nr_string_width]) do
def write_toc!(toc_list)
levels_indent_list = toc_indent_list(toc_list)
toc_list.each do |toc_item|
with_margin(styles.toc_item_margins(toc_item[:level])) do
write_toc_item! toc_item, levels_indent_list
end
end
end
def write_toc_item_subject!(toc_item, indent, subject_style)
pdf.indent(indent, toc_item[:page_nr_string_width]) do
pdf.formatted_text([subject_style.merge({ text: toc_item[:title] })])
end
end
def write_toc_item!(toc_item, max_level_width)
y = pdf.y
def toc_indent_list_flat(levels, level_max_widths)
levels_max_width = level_max_widths.max
levels.map do |_|
{ level_indent: 0, subject_index: levels_max_width }
end
end
def toc_indent_list_stairs(levels, level_max_widths)
indent_list = []
levels.each do |level|
level_indent = level <= 1 ? 0 : indent_list.last[:subject_index]
subject_index = level_indent + level_max_widths[level - 1]
indent_list.push({ level_indent:, subject_index: })
end
indent_list
end
def toc_indent_list_third_level(levels, level_max_widths)
indent_list = []
first_section = level_max_widths[0..1].max || 0
second_section = level_max_widths[2..].max || 0
levels.each do |level|
if level < 3
indent_list.push({ level_indent: 0, subject_index: first_section })
else
indent_list.push({ level_indent: first_section, subject_index: first_section + second_section })
end
end
indent_list
end
def toc_indent_list(toc_list)
levels = toc_list.pluck(:level).uniq.sort
level_max_widths = levels.map do |level|
toc_list.select { |item| item[:level] == level }.pluck(:level_string_width).max
end
mode = (styles.toc_indent_mode || :flat).to_sym
case mode
when :stairs
toc_indent_list_stairs(levels, level_max_widths)
when :third_level
toc_indent_list_third_level(levels, level_max_widths)
else
toc_indent_list_flat(levels, level_max_widths)
end
end
def build_toc_item_styles(toc_item)
toc_item_style = styles.toc_item(toc_item[:level])
part_style = toc_item_style.clone
font_styles = part_style.delete(:styles) || []
part_style[:style] = font_styles[0] unless font_styles.empty?
write_part_float(toc_item[:level_string], part_style)
write_part_float(toc_item[:page_nr_string], part_style.merge({ align: :right }))
write_toc_item_subject!(toc_item, max_level_width, toc_item_style)
[part_style, toc_item_style]
end
rect = [pdf.bounds.absolute_right, pdf.y, pdf.bounds.absolute_left, y]
def write_toc_item!(toc_item, levels_indent_list)
y_start_position = pdf.y
part_style, toc_item_style = build_toc_item_styles(toc_item)
indent = levels_indent_list[toc_item[:level] - 1]
write_part_float(indent[:level_indent], toc_item[:level_string], part_style)
write_part_float(0, toc_item[:page_nr_string], part_style.merge({ align: :right }))
write_toc_item_subject!(toc_item, indent[:subject_index], toc_item_style)
write_toc_item_link(toc_item, y_start_position)
end
def write_toc_item_link(toc_item, y_start_position)
rect = [pdf.bounds.absolute_right, pdf.y, pdf.bounds.absolute_left, y_start_position]
pdf.link_annotation(rect, Border: [0, 0, 0], Dest: toc_item[:id].to_s)
end
end
@@ -53,31 +53,28 @@ module WorkPackage::PDFExport::WorkPackageDetail
private
def write_work_package_subject!(work_package, level_path)
text_style = styles.wp_subject(level_path.length)
with_margin(styles.wp_detail_subject_margins) do
link_target_at_current_y(work_package.id)
level_string_width = write_work_package_level!(level_path)
level_string_width = write_work_package_level!(level_path, text_style)
title = get_column_value work_package, :subject
@pdf.indent(level_string_width) do
pdf.formatted_text([styles.wp_subject.merge({ text: title })])
pdf.formatted_text([text_style.merge({ text: title })])
end
end
end
def write_work_package_level!(level_path)
def write_work_package_level!(level_path, text_style)
return 0 if level_path.empty?
level_string = "#{level_path.join('.')}. "
level_string_width = measure_text_width(level_string, styles.wp_subject)
@pdf.float { @pdf.formatted_text([styles.wp_subject.merge({ text: level_string })]) }
level_string_width = measure_text_width(level_string, text_style)
@pdf.float { @pdf.formatted_text([text_style.merge({ text: level_string })]) }
level_string_width
end
def write_attributes_table!(work_package)
rows = if respond_to?(:column_objects)
build_columns_table_rows(work_package)
else
build_attributes_table_rows(work_package)
end
rows = attribute_table_rows(work_package)
with_margin(styles.wp_attributes_table_margins) do
pdf.table(
rows,
@@ -94,54 +91,62 @@ module WorkPackage::PDFExport::WorkPackageDetail
widths.map { |w| w * ratio }
end
def build_columns_table_rows(work_package)
list = column_objects.reject { |column| column.name == :subject }
def attribute_table_rows(work_package)
list = attribute_data_list(work_package)
0.step(list.length - 1, 2).map do |i|
build_columns_table_cells(list[i], work_package) +
build_columns_table_cells(list[i + 1], work_package)
build_columns_table_cells(list[i]) +
build_columns_table_cells(list[i + 1])
end
end
def build_attributes_table_rows(work_package)
# get work package attribute table rows data [[label, value, label, value]]
attrs = %i[
def attribute_data_list(work_package)
list = if respond_to?(:column_objects)
attributes_list_by_columns
else
attributes_list_by_wp
end
list
.map { |entry| entry.merge({ value: get_column_value_cell(work_package, entry[:name]) }) }
.select { |attribute_data| can_show_attribute?(attribute_data) }
end
def can_show_attribute?(attribute_data)
attribute_data[:value].present?
end
def attributes_list_by_columns
column_objects
.reject { |column| column.name == :subject }
.map do |column|
{ label: column.caption || '', name: column.name }
end
end
def attributes_list_by_wp
col_names = %i[
id
updated_at
type
created_at
status
due_date
version
priority
duration
work
category
priority
assigned_to
responsible
]
0.step(attrs.length - 1, 2).map do |i|
build_attributes_table_cells(attrs[i], work_package) +
build_attributes_table_cells(attrs[i + 1], work_package)
col_names.map do |col_name|
{ label: WorkPackage.human_attribute_name(col_name), name: col_name }
end
end
def build_attributes_table_cells(attribute, work_package)
# get work package attribute table cell data: [label, value]
return ['', ''] if attribute.nil?
def build_columns_table_cells(attribute_data)
return ['', ''] if attribute_data.nil?
build_attributes_row(WorkPackage.human_attribute_name(attribute) || '', attribute.to_sym, work_package)
end
def build_columns_table_cells(column, work_package)
return ['', ''] if column.nil?
build_attributes_row(column.caption || '', column.name, work_package)
end
def build_attributes_row(label, col_name, work_package)
# get work package attribute table cell data: [label, value]
[
pdf.make_cell(label.upcase, styles.wp_attributes_table_label_cell),
get_column_value_cell(work_package, col_name)
pdf.make_cell(attribute_data[:label].upcase, styles.wp_attributes_table_label_cell),
attribute_data[:value]
]
end
@@ -216,7 +216,7 @@ class WorkPackage::PDFExport::WorkPackageListToPdf < WorkPackage::Exports::Query
end
def should_be_batched?(work_packages)
batch_supported? && with_descriptions? && with_attachments? && (work_packages.length > @work_packages_per_batch)
batch_supported? && with_descriptions? && with_images? && (work_packages.length > @work_packages_per_batch)
end
def project
@@ -224,11 +224,19 @@ class WorkPackage::PDFExport::WorkPackageListToPdf < WorkPackage::Exports::Query
end
def title
# export filename expects to be "project.name - query.name"
"#{project ? "#{project} - #{heading}" : heading}.pdf"
# <project>_<querytitle>_<YYYY-MM-DD>_<HH-MM>.pdf
build_pdf_filename(project ? "#{project}_#{heading}" : heading)
end
def heading
query.name || I18n.t(:label_work_package_plural)
end
def footer_title
heading
end
def with_images?
options[:show_images]
end
end
@@ -58,8 +58,6 @@ class WorkPackage::PDFExport::WorkPackageToPdf < Exports::Exporter
error(I18n.t(:error_pdf_failed_to_export, error: e.message))
end
private
def setup_page!
self.pdf = get_pdf(current_language)
@page_count = 0
@@ -74,10 +72,20 @@ class WorkPackage::PDFExport::WorkPackageToPdf < Exports::Exporter
end
def heading
"#{work_package.project} - ##{work_package.type} #{work_package.id}"
"#{work_package.type} ##{work_package.id} - #{work_package.subject}"
end
def footer_title
work_package.project.name
end
def title
"#{heading}.pdf"
# <project>_<type>_<ID>_<subject><YYYY-MM-DD>_<HH-MM>.pdf
build_pdf_filename([work_package.project, work_package.type,
"##{work_package.id}", work_package.subject].join('_'))
end
def with_images?
true
end
end
@@ -58,7 +58,7 @@ module WorkPackages::Scopes::IncludeSpentTime
end
def allowed_to_view_time_entries(user)
time_entries_table[:id].in(TimeEntry.visible(user).select(:id).arel)
time_entries_table[:id].in(TimeEntry.not_ongoing.visible(user).select(:id).arel)
end
def wp_table
+7 -14
View File
@@ -54,10 +54,11 @@ class AdminUserSeeder < Seeder
User.new.tap do |user|
user.admin = true
user.login = 'admin'
user.password = 'admin'
user.firstname = 'OpenProject'
user.lastname = 'Admin'
user.mail = ENV['ADMIN_EMAIL'].presence || 'admin@example.net'
user.password = Setting.seed_admin_user_password
first, last = Setting.seed_admin_user_name.split(' ', 2)
user.firstname = first
user.lastname = last
user.mail = Setting.seed_admin_user_mail
user.language = I18n.locale.to_s
user.status = User.statuses[:active]
user.force_password_change = force_password_change?
@@ -66,16 +67,8 @@ class AdminUserSeeder < Seeder
end
def force_password_change?
!Rails.env.development? && !force_password_change_disabled?
end
return false if Rails.env.development?
def force_password_change_disabled?
off_values = ["off", "false", "no", "0"]
off_values.include? ENV.fetch(force_password_change_env_switch_name, nil)
end
def force_password_change_env_switch_name
"OP_ADMIN_USER_SEEDER_FORCE_PASSWORD_CHANGE"
Setting.seed_admin_user_password_reset?
end
end
+1
View File
@@ -345,5 +345,6 @@ roles:
position: 6
permissions:
- :add_project
- :create_user
- :manage_user
- :manage_placeholder_user
+119
View File
@@ -0,0 +1,119 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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 EnvData
class LdapSeeder < Seeder
def seed_data!
print_status ' ↳ Creating LDAP connection' do
Setting.seed_ldap.each do |name, options|
ldap = LdapAuthSource.find_or_initialize_by(name:)
print_ldap_status(ldap)
upsert_settings(ldap, options)
update_filters(ldap, options['groupfilter'])
end
end
print_status ' ↳ Synchronizing LDAP connections' do
LdapGroups::SynchronizationJob.perform_now
end
end
def applicable?
Setting.seed_ldap.present?
end
private
# rubocop:disable Metrics/AbcSize
def upsert_settings(ldap, options)
ldap.host = options['host']
ldap.port = options['port']
ldap.tls_mode = options['security']
ldap.verify_peer = ActiveRecord::Type::Boolean.new.deserialize options.fetch('tls_verify', true)
ldap.onthefly_register = ActiveRecord::Type::Boolean.new.deserialize options.fetch('sync_users', false)
ldap.tls_certificate_string = options['tls_certificate'].presence
ldap.filter_string = options['filter'].presence
ldap.base_dn = options['basedn']
ldap.account = options['binduser']
ldap.account_password = options['bindpassword']
ldap.attr_login = options['login_mapping'].presence
ldap.attr_firstname = options['firstname_mapping'].presence
ldap.attr_lastname = options['lastname_mapping'].presence
ldap.attr_mail = options['mail_mapping'].presence
ldap.attr_admin = options['admin_mapping'].presence
ldap.save!
end
# rubocop:enable Metrics/AbcSize
def update_filters(ldap, filters)
return if filters.blank? && !LdapGroups::SynchronizedFilter.exists?(ldap_auth_source: ldap)
upsert_existing_filters(ldap, filters) if filters.present?
remove_not_found_filters(ldap, filters&.keys || [])
end
def upsert_existing_filters(ldap, filters)
filters.each do |name, options|
filter = ::LdapGroups::SynchronizedFilter.find_or_initialize_by(ldap_auth_source: ldap, name:)
print_ldap_status(filter)
filter.group_name_attribute = options.fetch('group_attribute', 'dn')
filter.sync_users = ActiveRecord::Type::Boolean.new.deserialize options.fetch('sync_users', false)
filter.filter_string = options['filter']
filter.base_dn = options['base']
filter.save!
end
end
def remove_not_found_filters(ldap, names)
not_found = ::LdapGroups::SynchronizedFilter
.where(ldap_auth_source: ldap)
.where.not(name: names)
return unless not_found.exists?
not_found_names = not_found.pluck(:name).join(', ')
print_status " - Removing LDAP filter #{not_found_names} no longer present in ENV"
not_found.destroy_all
end
def print_ldap_status(object)
if object.new_record?
print_status " - Creating new #{object.model_name.human} #{object.name} from ENV"
else
print_status " - Updating existing #{object.model_name.human} #{object.name} from ENV"
end
end
end
end
@@ -24,6 +24,14 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class EnvDataSeeder < CompositeSeeder
def data_seeder_classes
[
EnvData::LdapSeeder
]
end
require 'open_project/pdf_export'
def namespace
'EnvData'
end
end
+6
View File
@@ -63,6 +63,7 @@ class RootSeeder < Seeder
seed_demo_data
seed_development_data if seed_development_data?
seed_plugins_data
seed_env_data
end
end
@@ -140,6 +141,11 @@ class RootSeeder < Seeder
DemoDataSeeder.new(seed_data).seed!
end
def seed_env_data
print_status '*** Seeding data from environment variables'
EnvDataSeeder.new(seed_data).seed!
end
def seed_development_data
print_status '*** Seeding development data'
require 'factory_bot'

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