mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
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:
+1
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -241,6 +241,7 @@ RSpec/ContextWording:
|
||||
- 'on'
|
||||
- to
|
||||
- unless
|
||||
- via
|
||||
- when
|
||||
- with
|
||||
- within
|
||||
|
||||
@@ -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
@@ -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
@@ -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'
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+3
-3
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
+6
-4
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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!
|
||||
|
||||
@@ -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
@@ -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:)
|
||||
|
||||
@@ -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
@@ -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?
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -345,5 +345,6 @@ roles:
|
||||
position: 6
|
||||
permissions:
|
||||
- :add_project
|
||||
- :create_user
|
||||
- :manage_user
|
||||
- :manage_placeholder_user
|
||||
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user