Merge branch 'dev' into merge-release/17.1-20260213121048

This commit is contained in:
Dombi Attila
2026-02-13 22:20:57 +02:00
committed by GitHub
786 changed files with 14586 additions and 16935 deletions
+2
View File
@@ -41,5 +41,7 @@ frontend/node_modules
node_modules
# travis
vendor/bundle
# Local checkout; all-in-one copies hocuspocus from its dedicated image.
vendor/hocuspocus
/public/assets
/config/frontend_assets.manifest.json
+10
View File
@@ -5,6 +5,11 @@ updates:
schedule:
interval: "daily"
target-branch: "dev"
cooldown:
default-days: 5
semver-major-days: 30
semver-minor-days: 14
semver-patch-days: 5
groups:
angular:
patterns:
@@ -40,6 +45,11 @@ updates:
schedule:
interval: "daily"
target-branch: "dev"
cooldown:
default-days: 5
semver-major-days: 30
semver-minor-days: 14
semver-patch-days: 5
groups:
aws-gems:
patterns:
+1
View File
@@ -72,6 +72,7 @@ jobs:
samachon,
shiroginne,
toy,
tiroessler,
ulferts,
vonTronje,
vspielau,
+3 -1
View File
@@ -21,8 +21,10 @@ jobs:
REPOSITORY: opf/openproject-flavours
WORKFLOW_ID: ci.yml
REF_NAME: ${{ github.ref_name }}
THIS_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
PAYLOAD=$(jq -n --arg ref "$REF_NAME" '{"ref": "dev", "inputs": {"ref": $ref}}')
PAYLOAD=$(jq -n --arg ref "$REF_NAME" --arg triggered_by_url "$THIS_RUN_URL" \
'{"ref": "dev", "inputs": {"ref": $ref, "triggered_by_url": $triggered_by_url}}')
curl -i --fail-with-body -H"authorization: Bearer $TOKEN" \
-XPOST -H"Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/$REPOSITORY/actions/workflows/$WORKFLOW_ID/dispatches \
+6 -17
View File
@@ -255,23 +255,12 @@ jobs:
if [ -d vendor/bundle.bak ]; then
mv vendor/bundle.bak vendor/bundle
fi
- 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' && matrix.target == 'all-in-one'
- name: Validate image
run: |
docker run \
--name openproject \
-d -p 8080:80 --platform ${{ matrix.platform }} \
-e SUPERVISORD_LOG_LEVEL=debug \
-e OPENPROJECT_LOGIN__REQUIRED=false \
-e OPENPROJECT_HTTPS=false \
${{ steps.build.outputs.imageid }}
sleep 60
docker logs openproject --tail 100
wget -O- --retry-on-http-error=503,502 --retry-connrefused http://localhost:8080/api/v3
./script/ci/docker_validate_image.sh \
--image "${{ steps.build.outputs.imageid }}" \
--target "${{ matrix.target }}" \
--platform "${{ matrix.platform }}"
- name: Push image
id: push
uses: docker/build-push-action@v6
@@ -355,7 +344,7 @@ jobs:
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ needs.setup.outputs.registry_image }}:${{ steps.meta.outputs.version }}
notify:
notify-failure:
needs: [setup, build, merge]
if: ${{ always() && contains(needs.*.result, 'failure') }}
uses: ./.github/workflows/email-notification.yml
+3 -1
View File
@@ -38,6 +38,7 @@ jobs:
BASE_REF: ${{ github.base_ref }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
REF_NAME: ${{ github.ref_name }}
THIS_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# ref:
# * on push this will be `dev` or a specific release branch (e.g. release/16.1) - there is always a matching branch for that downstream
# * on pull_request we use the PR branch's base (e.g. dev or a release branch) to match the above
@@ -64,7 +65,8 @@ jobs:
exit 0
fi
PAYLOAD=$(jq -n --arg ref "$REF" --arg core_ref "$CORE_REF" '{"ref": $ref, "inputs": {"core_ref": $core_ref}}')
PAYLOAD=$(jq -n --arg ref "$REF" --arg core_ref "$CORE_REF" --arg triggered_by_url "$THIS_RUN_URL" \
'{"ref": $ref, "inputs": {"core_ref": $core_ref, "triggered_by_url": $triggered_by_url}}')
OUTPUT_FILE=/tmp/request-output
echo "Triggering $WORKFLOW_ID workflow on $REPOSITORY branch '$REF', which will check out core branch '$CORE_REF'"
+4 -2
View File
@@ -44,8 +44,9 @@ jobs:
- name: Prepare docker-compose files
run: |
cp ./docker/pullpreview/docker-compose.yml ./docker-compose.pullpreview.yml
cp ./docker/pullpreview/Caddyfile ./Caddyfile
cp ./docker/prod/Dockerfile ./Dockerfile
- uses: pullpreview/action@v5
- uses: pullpreview/action@v6
with:
# allows to ssh to the instance using our GitHub ssh key
# command like: ssh ec2-user@pr-19375-trial-ip-3-127-219-135.my.preview.run
@@ -54,7 +55,8 @@ jobs:
instance_type: xlarge
ports: 80,443,8080
default_port: 443
ttl: 10d
ttl: 8d
dns: my.opf.run
env:
AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.AWS_SECRET_ACCESS_KEY }}"
+131
View File
@@ -0,0 +1,131 @@
name: Test seeding in all locales
on:
schedule:
- cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC
workflow_dispatch:
inputs:
ref:
description: 'Git ref to test (branch, tag, or SHA). Defaults to latest release branch.'
required: false
type: string
permissions:
contents: read
jobs:
prepare:
if: github.repository == 'opf/openproject'
name: Prepare
runs-on: ubuntu-latest
outputs:
locales: ${{ steps.list.outputs.locales }}
ref: ${{ steps.use_input_or_find_latest_release.outputs.ref }}
steps:
- name: Determine git ref to test seeding in
id: use_input_or_find_latest_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INPUT_REF: ${{ inputs.ref }}
run: |
if [ -n "$INPUT_REF" ]; then
echo "ref=$INPUT_REF" >> "$GITHUB_OUTPUT"
else
BRANCH=$(gh api repos/opf/openproject/branches --paginate --jq '.[].name' | grep '^release/' | sort --version-sort | tail -1)
if [ -z "$BRANCH" ]; then
echo "Error: no release branch found"
exit 1
fi
echo "Found latest release branch: $BRANCH"
echo "ref=$BRANCH" >> "$GITHUB_OUTPUT"
fi
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ steps.use_input_or_find_latest_release.outputs.ref }}
- name: Print ref to summary
run: |
SHA=$(git rev-parse HEAD)
SHORT_SHA=$(git rev-parse --short HEAD)
echo "Testing seeding on **${{ steps.use_input_or_find_latest_release.outputs.ref }}** ([$SHORT_SHA](https://github.com/opf/openproject/commit/$SHA))" >> "$GITHUB_STEP_SUMMARY"
- name: List available locales
id: list
run: |
locales=$(ruby script/i18n/test_seed_all_locales --list)
echo "locales=$locales" >> "$GITHUB_OUTPUT"
seed:
needs: prepare
if: github.repository == 'opf/openproject'
name: Seed in ${{ matrix.locale }} for ${{ matrix.edition }} edition
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
locale: ${{ fromJson(needs.prepare.outputs.locales) }}
edition: [standard, bim]
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/openproject_seed_test
RAILS_ENV: development
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.ref }}
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y libpq-dev
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Configure database
run: |
cat > config/database.yml <<'EOF'
development:
url: <%= ENV["DATABASE_URL"] %>
EOF
- name: Seed in locale ${{ matrix.locale }} for ${{ matrix.edition }} edition
env:
SILENCE_SQL_LOGS: 1
OPENPROJECT_EDITION: ${{ matrix.edition }}
run: ruby script/i18n/test_seed_all_locales ${{ matrix.locale }}
notify-failure:
needs: [prepare, seed]
if: ${{ always() && contains(needs.*.result, 'failure') }}
uses: ./.github/workflows/email-notification.yml
secrets: inherit
with:
to: operations@openproject.com
subject: "Seeding with some locales failed on ${{ needs.prepare.outputs.ref }}"
body: |
The seed-all-locales workflow has failed.
Branch: ${{ needs.prepare.outputs.ref }}
Editions tested: standard, bim
Trigger: ${{ github.event_name }}
Some locale/edition combinations failed to seed correctly.
Check the workflow run for details on which locales failed:
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+5
View File
@@ -198,6 +198,11 @@ Rails/I18nLocaleAssignment:
Exclude:
- "spec/**/*.rb"
Rails/I18nLocaleTexts:
Enabled: true
Exclude:
- "spec/**/*.rb"
# We have config.active_record.belongs_to_required_by_default = false ,
# which means, we do have to declare presence validators on belongs_to relations.
Rails/RedundantPresenceValidationOnBelongsTo:
+1 -2
View File
@@ -4,7 +4,7 @@ We are pleased that you are thinking about contributing to OpenProject! This gui
## Get in touch
Please get in touch with us using our [development forum](https://community.openproject.org/projects/openproject/boards/7) or send us an email to [info@openproject.com](mailto:info@openproject.com).
Please get in touch with us using our [OpenProject community platform](https://community.openproject.org/) or send us an email to [info@openproject.com](mailto:info@openproject.com).
## Issue tracking and coordination
@@ -12,7 +12,6 @@ We eat our own ice cream so we use OpenProject for roadmap planning and team col
- [Product roadmap](https://community.openproject.org/projects/openproject/roadmap)
- [Wish list](https://community.openproject.org/projects/openproject/work_packages?query_id=180)
- [Bug backlog board](https://community.openproject.org/projects/openproject/boards/2905)
- [Report a bug](https://www.openproject.org/docs/development/report-a-bug/)
- [Submit a feature idea](https://www.openproject.org/docs/development/submit-feature-idea/)
+1 -1
View File
@@ -1,6 +1,6 @@
OpenProject is an open source project management software.
Copyright (C) 2012-2025 the OpenProject GmbH
Copyright (C) 2012-2026 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
+4 -4
View File
@@ -41,7 +41,7 @@ gem "activemodel-serializers-xml", "~> 1.0.1"
gem "activerecord-import", "~> 2.2.0"
gem "activerecord-session_store", "~> 2.2.0"
gem "ox"
gem "rails", "~> 8.0.4"
gem "rails", "~> 8.1.2"
gem "responders", "~> 3.2"
gem "ffi", "~> 1.15"
@@ -163,7 +163,7 @@ gem "matrix", "~> 0.4.3"
gem "mcp", "~> 0.4.0"
gem "meta-tags", "~> 2.22.2"
gem "meta-tags", "~> 2.22.3"
gem "paper_trail", "~> 17.0.0"
@@ -200,7 +200,7 @@ gem "fog-aws"
gem "aws-sdk-core", "~> 3.241"
# File upload via fog + screenshots on travis
gem "aws-sdk-s3", "~> 1.211"
gem "aws-sdk-s3", "~> 1.213"
gem "openproject-token", "~> 8.6.0"
@@ -428,4 +428,4 @@ end
gem "openproject-octicons", "~>19.32.0"
gem "openproject-octicons_helper", "~>19.32.0"
gem "openproject-primer_view_components", "~>0.80.2"
gem "openproject-primer_view_components", "~>0.81.1"
+102 -98
View File
@@ -203,7 +203,7 @@ PATH
remote: modules/two_factor_authentication
specs:
openproject-two_factor_authentication (1.0.0)
aws-sdk-sns (>= 1.101, < 1.112)
aws-sdk-sns (>= 1.101, < 1.113)
messagebird-rest (>= 1.4.2, < 5.1.0)
rotp (~> 6.1)
webauthn (~> 3.0)
@@ -223,29 +223,31 @@ GEM
remote: https://rubygems.org/
specs:
Ascii85 (2.0.1)
actioncable (8.0.4)
actionpack (= 8.0.4)
activesupport (= 8.0.4)
action_text-trix (2.1.16)
railties
actioncable (8.1.2)
actionpack (= 8.1.2)
activesupport (= 8.1.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.4)
actionpack (= 8.0.4)
activejob (= 8.0.4)
activerecord (= 8.0.4)
activestorage (= 8.0.4)
activesupport (= 8.0.4)
actionmailbox (8.1.2)
actionpack (= 8.1.2)
activejob (= 8.1.2)
activerecord (= 8.1.2)
activestorage (= 8.1.2)
activesupport (= 8.1.2)
mail (>= 2.8.0)
actionmailer (8.0.4)
actionpack (= 8.0.4)
actionview (= 8.0.4)
activejob (= 8.0.4)
activesupport (= 8.0.4)
actionmailer (8.1.2)
actionpack (= 8.1.2)
actionview (= 8.1.2)
activejob (= 8.1.2)
activesupport (= 8.1.2)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.4)
actionview (= 8.0.4)
activesupport (= 8.0.4)
actionpack (8.1.2)
actionview (= 8.1.2)
activesupport (= 8.1.2)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@@ -256,33 +258,34 @@ GEM
actionpack-xml_parser (2.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
actiontext (8.0.4)
actionpack (= 8.0.4)
activerecord (= 8.0.4)
activestorage (= 8.0.4)
activesupport (= 8.0.4)
actiontext (8.1.2)
action_text-trix (~> 2.1.15)
actionpack (= 8.1.2)
activerecord (= 8.1.2)
activestorage (= 8.1.2)
activesupport (= 8.1.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.4)
activesupport (= 8.0.4)
actionview (8.1.2)
activesupport (= 8.1.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
active_record_doctor (2.0.1)
activerecord (>= 7.0.0)
activejob (8.0.4)
activesupport (= 8.0.4)
activejob (8.1.2)
activesupport (= 8.1.2)
globalid (>= 0.3.6)
activemodel (8.0.4)
activesupport (= 8.0.4)
activemodel (8.1.2)
activesupport (= 8.1.2)
activemodel-serializers-xml (1.0.3)
activemodel (>= 5.0.0.a)
activesupport (>= 5.0.0.a)
builder (~> 3.1)
activerecord (8.0.4)
activemodel (= 8.0.4)
activesupport (= 8.0.4)
activerecord (8.1.2)
activemodel (= 8.1.2)
activesupport (= 8.1.2)
timeout (>= 0.4.0)
activerecord-import (2.2.0)
activerecord (>= 4.2)
@@ -294,20 +297,20 @@ GEM
cgi (>= 0.3.6)
rack (>= 2.0.8, < 4)
railties (>= 7.0)
activestorage (8.0.4)
actionpack (= 8.0.4)
activejob (= 8.0.4)
activerecord (= 8.0.4)
activesupport (= 8.0.4)
activestorage (8.1.2)
actionpack (= 8.1.2)
activejob (= 8.1.2)
activerecord (= 8.1.2)
activesupport (= 8.1.2)
marcel (~> 1.0)
activesupport (8.0.4)
activesupport (8.1.2)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
json
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
@@ -339,8 +342,8 @@ GEM
awesome_nested_set (3.9.0)
activerecord (>= 4.0.0, < 8.2)
aws-eventstream (1.4.0)
aws-partitions (1.1202.0)
aws-sdk-core (3.241.3)
aws-partitions (1.1210.0)
aws-sdk-core (3.241.4)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -348,15 +351,15 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.120.0)
aws-sdk-core (~> 3, >= 3.241.3)
aws-sdk-kms (1.121.0)
aws-sdk-core (~> 3, >= 3.241.4)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.211.0)
aws-sdk-core (~> 3, >= 3.241.3)
aws-sdk-s3 (1.213.0)
aws-sdk-core (~> 3, >= 3.241.4)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sdk-sns (1.111.0)
aws-sdk-core (~> 3, >= 3.241.3)
aws-sdk-sns (1.112.0)
aws-sdk-core (~> 3, >= 3.241.4)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
@@ -556,7 +559,7 @@ GEM
factory_bot_rails (6.5.1)
factory_bot (~> 6.5)
railties (>= 6.1.0)
faraday (2.14.0)
faraday (2.14.1)
faraday-net_http (>= 2.0, < 3.5)
json
logger
@@ -724,7 +727,7 @@ GEM
reline (>= 0.4.2)
iso8601 (0.13.0)
jmespath (1.6.2)
json (2.18.0)
json (2.18.1)
json-jwt (1.17.0)
activesupport (>= 4.2)
aes_key_wrap
@@ -803,8 +806,8 @@ GEM
json_rpc_handler (~> 0.1)
messagebird-rest (5.0.0)
jwt (< 4)
meta-tags (2.22.2)
actionpack (>= 6.0.0, < 8.2)
meta-tags (2.22.3)
actionpack (>= 6.0.0)
method_source (1.1.0)
mime-types (3.7.0)
logger
@@ -879,7 +882,7 @@ GEM
actionview
openproject-octicons (= 19.32.0)
railties
openproject-primer_view_components (0.80.2)
openproject-primer_view_components (0.81.1)
actionview (>= 7.2.0)
activesupport (>= 7.2.0)
openproject-octicons (>= 19.30.1)
@@ -1106,7 +1109,7 @@ GEM
prawn-table (0.2.2)
prawn (>= 1.3.0, < 3.0.0)
prettyprint (0.2.0)
prism (1.8.0)
prism (1.9.0)
prometheus-client-mmap (1.5.0)
base64
bigdecimal
@@ -1199,20 +1202,20 @@ GEM
rackup (1.0.1)
rack (< 3)
webrick
rails (8.0.4)
actioncable (= 8.0.4)
actionmailbox (= 8.0.4)
actionmailer (= 8.0.4)
actionpack (= 8.0.4)
actiontext (= 8.0.4)
actionview (= 8.0.4)
activejob (= 8.0.4)
activemodel (= 8.0.4)
activerecord (= 8.0.4)
activestorage (= 8.0.4)
activesupport (= 8.0.4)
rails (8.1.2)
actioncable (= 8.1.2)
actionmailbox (= 8.1.2)
actionmailer (= 8.1.2)
actionpack (= 8.1.2)
actiontext (= 8.1.2)
actionview (= 8.1.2)
activejob (= 8.1.2)
activemodel (= 8.1.2)
activerecord (= 8.1.2)
activestorage (= 8.1.2)
activesupport (= 8.1.2)
bundler (>= 1.15.0)
railties (= 8.0.4)
railties (= 8.1.2)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -1227,9 +1230,9 @@ GEM
rails-i18n (8.1.0)
i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9)
railties (8.0.4)
actionpack (= 8.0.4)
activesupport (= 8.0.4)
railties (8.1.2)
actionpack (= 8.1.2)
activesupport (= 8.1.2)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -1423,7 +1426,7 @@ GEM
unicode-display_width (>= 1.1.1, < 4)
test-prof (1.4.4)
text-hyphen (1.5.0)
thor (1.4.0)
thor (1.5.0)
thread_safe (0.3.6)
timecop (0.9.10)
timeout (0.6.0)
@@ -1548,7 +1551,7 @@ DEPENDENCIES
auto_strip_attributes (~> 2.5)
awesome_nested_set (~> 3.9.0)
aws-sdk-core (~> 3.241)
aws-sdk-s3 (~> 1.211)
aws-sdk-s3 (~> 1.213)
axe-core-rspec
bcrypt (~> 3.1.6)
bootsnap (~> 1.20.0)
@@ -1623,7 +1626,7 @@ DEPENDENCIES
matrix (~> 0.4.3)
mcp (~> 0.4.0)
md_to_pdf!
meta-tags (~> 2.22.2)
meta-tags (~> 2.22.3)
mini_magick (~> 5.3.0)
multi_json (~> 1.19.0)
my_page!
@@ -1652,7 +1655,7 @@ DEPENDENCIES
openproject-octicons (~> 19.32.0)
openproject-octicons_helper (~> 19.32.0)
openproject-openid_connect!
openproject-primer_view_components (~> 0.80.2)
openproject-primer_view_components (~> 0.81.1)
openproject-recaptcha!
openproject-reporting!
openproject-storages!
@@ -1686,7 +1689,7 @@ DEPENDENCIES
rack-test (~> 2.2.0)
rack-timeout (~> 0.7.0)
rack_session_access
rails (~> 8.0.4)
rails (~> 8.1.2)
rails-controller-testing (~> 1.0.2)
rails-i18n (~> 8.1.0)
rbtrace
@@ -1756,23 +1759,24 @@ DEPENDENCIES
CHECKSUMS
Ascii85 (2.0.1) sha256=15cb5d941808543cbb9e7e6aea3c8ec3877f154c3461e8b3673e97f7ecedbe5a
actioncable (8.0.4) sha256=aadb2bf2977b666cfeaa7dee66fd50e147559f78a8d55f6169e913502475e09f
actionmailbox (8.0.4) sha256=ed0b634a502fb63d1ba01ae025772e9d0261b7ba12e66389c736fcf4635cd80f
actionmailer (8.0.4) sha256=3b9270d8e19f0afb534b11c52f439937dc30028adcbbae2b244f3383ce75de4b
actionpack (8.0.4) sha256=0364c7582f32c8f404725fa30d3f6853f834c5f4964afd4a072b848c8a23cddb
action_text-trix (2.1.16) sha256=f645a2c21821b8449fd1d6770708f4031c91a2eedf9ef476e9be93c64e703a8a
actioncable (8.1.2) sha256=dc31efc34cca9cdefc5c691ddb8b4b214c0ea5cd1372108cbc1377767fb91969
actionmailbox (8.1.2) sha256=058b2fb1980e5d5a894f675475fcfa45c62631103d5a2596d9610ec81581889b
actionmailer (8.1.2) sha256=f4c1d2060f653bfe908aa7fdc5a61c0e5279670de992146582f2e36f8b9175e9
actionpack (8.1.2) sha256=ced74147a1f0daafaa4bab7f677513fd4d3add574c7839958f7b4f1de44f8423
actionpack-xml_parser (2.0.1) sha256=40cb461ee99445314ab580a783fb7413580deb8b28113c9e70ecd7c1b334d5e6
actiontext (8.0.4) sha256=40b3970268ac29b865685456b2586df5052d068fd0cb04acb2291e737cea2340
actionview (8.0.4) sha256=5bd3c41ee7a59e14cf062bb5e4ee53c9a253d12fc13c8754cae368012e1a1648
actiontext (8.1.2) sha256=0bf57da22a9c19d970779c3ce24a56be31b51c7640f2763ec64aa72e358d2d2d
actionview (8.1.2) sha256=80455b2588911c9b72cec22d240edacb7c150e800ef2234821269b2b2c3e2e5b
active_record_doctor (2.0.1) sha256=7af0ac02195385c8f2f67d0e4ebe72b1fc79d65eaaf329e0db07f4d12a84069a
activejob (8.0.4) sha256=cbc8a85d0e168cb90a5629c8a36fe2d08ba840103d3aed3eee0c7beb784fccce
activemodel (8.0.4) sha256=8f4e4fac3cd104b1bf30419c3745206f6f724c0e2902a939b4113f4c90730dfd
activejob (8.1.2) sha256=908dab3713b101859536375819f4156b07bdf4c232cc645e7538adb9e302f825
activemodel (8.1.2) sha256=e21358c11ce68aed3f9838b7e464977bc007b4446c6e4059781e1d5c03bcf33e
activemodel-serializers-xml (1.0.3) sha256=fa1b16305e7254cc58a59c68833e3c0a593a59c8ab95d3be5aaea7cd9416c397
activerecord (8.0.4) sha256=bda32c171799e5ca5460447d3b7272ed14447244e2497abf2107f87fc44cbf32
activerecord (8.1.2) sha256=acfbe0cadfcc50fa208011fe6f4eb01cae682ebae0ef57145ba45380c74bcc44
activerecord-import (2.2.0) sha256=f8ca99b196e50775723d1f1d192c379f656378dc9f5628240992a0d78807fa4b
activerecord-nulldb-adapter (1.2.2) sha256=01e0b2e49af11ad56a92e274a3d8c9fb3c50a12a5460218c4c4b45355d9ef968
activerecord-session_store (2.2.0) sha256=65918054573683bf4f87af89e765e1fece14c9d71cfac1f11abe4687c96e2743
activestorage (8.0.4) sha256=47f312962fc898c1669f20cf7448d19668a5547f4a5f64e59a837d9d3f64a043
activesupport (8.0.4) sha256=894a3a6c7733b5fae5a7df3acd76c4b563f38687df8a04fa3cbd25360f3fe95a
activestorage (8.1.2) sha256=8a63a48c3999caeee26a59441f813f94681fc35cc41aba7ce1f836add04fba76
activesupport (8.1.2) sha256=88842578ccd0d40f658289b0e8c842acfe9af751afee2e0744a7873f50b6fdae
acts_as_list (1.2.6) sha256=8345380900b7bee620c07ad00991ccee59af3d8c9e8574f426e321da2865fdc8
acts_as_tree (2.9.1) sha256=b869eb10a8de38616b64ffcf9e882d3d99c8e06909c4057078a76c3b89a9a2f3
addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057
@@ -1788,11 +1792,11 @@ CHECKSUMS
auto_strip_attributes (2.6.0) sha256=a7e2e0cf744de2bcd947fd68014220702bcc88c81274c1cd9ce6f7316aae39b0
awesome_nested_set (3.9.0) sha256=3ce99e816550f97f4de118e621630070aacf24928b920fe4a68846578a8daaed
aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b
aws-partitions (1.1202.0) sha256=c8aa0f134a23464c61cfd00edfb4b6d968b01847a8b591d4dcc0c63a4897c301
aws-sdk-core (3.241.3) sha256=c7c445ecf1c601c860fd537458b2eb8df0c5df01e63c371849e6594e6b1d4f47
aws-sdk-kms (1.120.0) sha256=a206ac6f62efbe971f802e8399d2702496a5c5bc800abcf94ead87bdddfdfd80
aws-sdk-s3 (1.211.0) sha256=2ae5feb09ff4862462824f267b76601ed16922a15de56cf51e4fa99bc5b3f519
aws-sdk-sns (1.111.0) sha256=195edbd6953d4caa2748bd4861e0fe8df54d90aadf07a0ac268987b946c7e5be
aws-partitions (1.1210.0) sha256=04143b868f8b3fc481f68552df6a430f1083a56e2afec5a6bc5c89532ab423fe
aws-sdk-core (3.241.4) sha256=a42ccba8c24ea9800e7b6c40aa201c967458f7c460044a6eebf64fbf1226e4fd
aws-sdk-kms (1.121.0) sha256=d563c1cfb4b5754efbc671216c8eca875338748adad0f42518c28dfa0a2d01e0
aws-sdk-s3 (1.213.0) sha256=af596ccf544582406db610e95cc9099276eaf03142f57a2f30f76940e598e50d
aws-sdk-sns (1.112.0) sha256=aff1b1b5bbcb4229599221c558a41790c1cd1a1fed47ac3d27d27512ad24b254
aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00
axe-core-api (4.11.0) sha256=3d9c94e3c8f8f9b8f154a3ce036b3dec2dabf7bb7de5e51d663b18bd8a0d691b
axe-core-rspec (4.11.0) sha256=3c3e3ef3863d9f5243e056b7da328932c0b6682dda299bb4bd74d760641486d7
@@ -1881,7 +1885,7 @@ CHECKSUMS
excon (1.3.2) sha256=a089babe98638e58042a7d542b2bbd183304527e33d612b6dde22fa491a544a5
factory_bot (6.5.6) sha256=12beb373214dccc086a7a63763d6718c49769d5606f0501e0a4442676917e077
factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68
faraday (2.14.0) sha256=8699cfe5d97e55268f2596f9a9d5a43736808a943714e3d9a53e6110593941cd
faraday (2.14.1) sha256=a43cceedc1e39d188f4d2cdd360a8aaa6a11da0c407052e426ba8d3fb42ef61c
faraday-follow_redirects (0.5.0) sha256=5cde93c894b30943a5d2b93c2fe9284216a6b756f7af406a1e55f211d97d10ad
faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c
fastimage (2.4.0) sha256=5fce375e27d3bdbb46c18dbca6ba9af29d3304801ae1eb995771c4796c5ac7e8
@@ -1948,7 +1952,7 @@ CHECKSUMS
irb (1.16.0) sha256=2abe56c9ac947cdcb2f150572904ba798c1e93c890c256f8429981a7675b0806
iso8601 (0.13.0) sha256=298c2b15b7be5fa95a1372813d36a2257656cd8e906dfbc1f5cb409851425aa2
jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1
json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505
json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986
json-jwt (1.17.0) sha256=6ff99026b4c54281a9431179f76ceb81faa14772d710ef6169785199caadc4cc
json-schema (4.3.1) sha256=d5e68dc32b94408d0b06ad04f9382ccbb6fe5a44910e066f8547f56c471a7825
json_rpc_handler (0.1.1) sha256=ea248c8cb4d5490dde320db316ac5e3caf8137a20b5ff9035a4bfc1d19438d90
@@ -1975,7 +1979,7 @@ CHECKSUMS
mcp (0.4.0) sha256=4d1dd2b99fbd81a5fdc808d258c38a4f57dd69751ee1e5f62b3ab40e31625a36
md_to_pdf (0.2.5)
messagebird-rest (5.0.0) sha256=da4cc1efba3d5e4aa021fad07426c2cb6b326ce5670da5104bb8f6056a39d59c
meta-tags (2.22.2) sha256=7fe78af4a92be12091f473cb84a21f6bddbd37f24c4413172df76cd14fff9e83
meta-tags (2.22.3) sha256=41ead5437140869717cbdd659cc6f1caa3e498b3e74b03ed63503b5b38ed504f
method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5
mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56
mime-types-data (3.2025.0924) sha256=f276bca15e59f35767cbcf2bc10e023e9200b30bd6a572c1daf7f4cc24994728
@@ -2028,7 +2032,7 @@ CHECKSUMS
openproject-octicons (19.32.0) sha256=e9c908e7c4310d57e1dece8fc506339862f18b67b3b67d549e8f56a7b763d48b
openproject-octicons_helper (19.32.0) sha256=687a8b173c6436634397477c1f05b0a575e52745a5cc1aef03351272e73e3832
openproject-openid_connect (1.0.0)
openproject-primer_view_components (0.80.2) sha256=d36d9fd48857f3dbcdd0e7a408ef9ce01211a9b09e6186ad2413b300f2e50a8e
openproject-primer_view_components (0.81.1) sha256=ebda313d71f4c7e82b9a31e0d54b1e2b5ac3776816161fb61517ced1d945587d
openproject-recaptcha (1.0.0)
openproject-reporting (1.0.0)
openproject-storages (1.0.0)
@@ -2119,7 +2123,7 @@ CHECKSUMS
prawn (2.4.0) sha256=82062744f7126c2d77501da253a154271790254dfa8c309b8e52e79bc5de2abd
prawn-table (0.2.2) sha256=336d46e39e003f77bf973337a958af6a68300b941c85cb22288872dc2b36addb
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
prism (1.8.0) sha256=84453a16ef5530ea62c5f03ec16b52a459575ad4e7b9c2b360fd8ce2c39c1254
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
prometheus-client-mmap (1.5.0) sha256=361eb98d6c19ae0f44ae5e02f9e6750436fd92d1c501d1c69843609c1daf0117
prometheus-client-mmap (1.5.0-aarch64-linux-gnu) sha256=e7fe1a630dda83a237efb0eb4a29ee3da37922722fc89ecac6057a1187372c5d
prometheus-client-mmap (1.5.0-aarch64-linux-musl) sha256=897fa5d82150ddcb3bc30dfa7af0deb85930655500e71bd8879daa86b5e690ff
@@ -2149,12 +2153,12 @@ CHECKSUMS
rack-timeout (0.7.0) sha256=757337e9793cca999bb73a61fe2a7d4280aa9eefbaf787ce3b98d860749c87d9
rack_session_access (0.2.0) sha256=03eb98f2027429ccbbeb18556006dfb6d928b0557ad3770783b8e2f368198d6b
rackup (1.0.1) sha256=ba86604a28989fe1043bff20d819b360944ca08156406812dca6742b24b3c249
rails (8.0.4) sha256=364494a32d2dc3f9d5c135d036ce47e7776684bc6add73f1037ac2b1007962db
rails (8.1.2) sha256=5069061b23dfa8706b9f0159ae8b9d35727359103178a26962b868a680ba7d95
rails-controller-testing (1.0.5) sha256=741448db59366073e86fc965ba403f881c636b79a2c39a48d0486f2607182e94
rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d
rails-html-sanitizer (1.6.2) sha256=35fce2ca8242da8775c83b6ba9c1bcaad6751d9eb73c1abaa8403475ab89a560
rails-i18n (8.1.0) sha256=52d5fd6c0abef28d84223cc05647f6ae0fd552637a1ede92deee9545755b6cf3
railties (8.0.4) sha256=8203d853dcffab4abcdd05c193f101676a92068075464694790f6d8f72d5cb47
railties (8.1.2) sha256=1289ece76b4f7668fc46d07e55cc992b5b8751f2ad85548b7da351b8c59f8055
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
rake-compiler-dock (1.11.0) sha256=eab51f2cd533eb35cea6b624a75281f047123e70a64c58b607471bb49428f8c2
@@ -2237,7 +2241,7 @@ CHECKSUMS
terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2
test-prof (1.4.4) sha256=1a59513ed9d33a1f5ca17c0b89da4e70f60a91c83ec62e9a873dbb99141353ef
text-hyphen (1.5.0) sha256=c44a4533b8a554e7ff7c955e131bcccc78a0b4c56ce1d73f2c8c11f43b075a06
thor (1.4.0) sha256=8763e822ccb0f1d7bee88cde131b19a65606657b847cc7b7b4b82e772bcd8a3d
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
thread_safe (0.3.6) sha256=9ed7072821b51c57e8d6b7011a8e282e25aeea3a4065eab326e43f66f063b05a
timecop (0.9.10) sha256=12ba45ce57cdcf6b1043cb6cdffa6381fd89ce10d369c28a7f6f04dc1b0cd8eb
timeout (0.6.0) sha256=6d722ad619f96ee383a0c557ec6eb8c4ecb08af3af62098a0be5057bf00de1af
+1
View File
@@ -7,6 +7,7 @@
@import "open_project/common/attribute_help_text_component"
@import "open_project/common/attribute_help_text_caption_component"
@import "open_project/common/attribute_label_component"
@import "open_project/common/inplace_edit_fields/index"
@import "open_project/common/submenu_component"
@import "open_project/common/main_menu_toggle_component"
@import "portfolios/details_component"
@@ -29,9 +29,9 @@ See COPYRIGHT and LICENSE files for more details.
<%=
render(Primer::OpenProject::PageHeader.new(test_selector: "custom-fields--page-header")) do |header|
header.with_title { @custom_field.attribute_in_database("name") }
header.with_title { page_title }
header.with_breadcrumbs(breadcrumbs_items)
header.with_breadcrumbs(breadcrumbs_items, selected_item_font_weight: :normal)
helpers.render_tab_header_nav(header, tabs, test_selector: :custom_field_detail_header)
end
@@ -76,11 +76,19 @@ module Admin
private
def page_title
concat @custom_field.attribute_in_database("name")
concat render(Primer::Beta::Text.new(color: :muted)) { " (#{helpers.label_for_custom_field_format(@custom_field.field_format)})" }
end
def breadcrumbs_items
[{ href: admin_index_path, text: t(:label_administration) },
{ href: custom_fields_path, text: t(:label_custom_field_plural) },
{ href: custom_fields_path(tab: @custom_field.type), text: I18n.t(@custom_field.type_name) },
@custom_field.attribute_in_database("name")]
[
{ href: admin_index_path, text: t(:label_administration) },
{ href: custom_fields_path, text: t(:label_custom_field_plural) },
{ href: custom_fields_path(tab: @custom_field.type), text: I18n.t(@custom_field.type_name) },
helpers.nested_breadcrumb_element(helpers.label_for_custom_field_format(model.field_format),
@custom_field.attribute_in_database("name"))
]
end
end
end
@@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%= form_with model: member.new_record? ? [member.project, member] : member,
<%= form_with model: [member.project, member],
builder: TabularFormBuilder,
html: form_html_options do |f| %>
<%= hidden_field_tag :page, params[:page].to_i %>
@@ -29,7 +29,7 @@
mobile_icon: :pencil,
mobile_label: t(:button_edit),
size: :medium,
href: edit_topic_path(@topic),
href: edit_project_forum_topic_path(@topic.forum.project, @topic.forum, @topic),
aria: { label: t(:button_edit) },
data: { test_selector: "message-edit-button" },
title: t(:button_edit)
@@ -46,7 +46,7 @@
mobile_icon: :trash,
mobile_label: t(:button_delete),
size: :medium,
href: topic_path(@topic),
href: project_forum_topic_path(@topic.forum.project, @topic.forum, @topic),
aria: { label: I18n.t(:button_delete) },
data: {
turbo_confirm: I18n.t(:text_are_you_sure),
@@ -56,7 +56,7 @@ module My
def token_available?
case token_type.to_s
when "Token::API" then Setting.rest_api_enabled?
when "Token::API" then Setting.api_tokens_enabled?
when "Token::ICalMeeting" then Setting.ical_enabled?
when "Token::RSS" then Setting.feeds_enabled?
else raise ArgumentError, "Unknown token type: #{token_type}"
@@ -37,11 +37,11 @@ module OpenProject
:description,
:lines,
:background_reference_id,
:formatted
:format
PARAGRAPH_CSS_CLASS = "op-uc-p"
def initialize(id, name, description, lines: 1, background_reference_id: "content", formatted: false, **args)
def initialize(id, name, description, lines: 1, background_reference_id: "content", format: true, **args)
super()
@id = id
@name = name
@@ -49,7 +49,7 @@ module OpenProject
@system_arguments = args
@lines = lines
@background_reference_id = background_reference_id
@formatted = formatted
@format = format
end
def short_text
@@ -61,7 +61,7 @@ module OpenProject
end
def full_text
@full_text ||= formatted ? description : helpers.format_text(description)
@full_text ||= format ? helpers.format_text(description) : description
end
def display_expand_button_value
@@ -0,0 +1,20 @@
<%= component_wrapper(tag: :div, class: "op-inplace-edit", data: { test_selector: wrapper_test_selector }) do %>
<% if display_field_component.present? && !enforce_edit_mode %>
<%= render display_field_component %>
<% else %>
<%= primer_form_with(
model:,
url: inplace_edit_field_update_path(model: model.class.name, id: model.id, attribute:),
method: :patch,
data: { turbo_stream: true }
) do |form|
render_field_component = ->(f) { render edit_field_component(f) } # The render_inline_form method looses context and thus does not know about the `field_component` method
system_arguments = @system_arguments
render_inline_form(form) do |f|
f.hidden name: "system_arguments_json", value: system_arguments.to_json
render_field_component.call(f)
end
end %>
<% end %>
<% end %>
@@ -0,0 +1,98 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject
module Common
class InplaceEditFieldComponent < ViewComponent::Base
include OpTurbo::Streamable
attr_reader :model, :attribute, :enforce_edit_mode
def initialize(model:, attribute:, enforce_edit_mode: false, **system_arguments)
super()
@model = model
@attribute = attribute
@enforce_edit_mode = enforce_edit_mode
@system_arguments = system_arguments
@system_arguments[:id] = system_arguments[:id] || SecureRandom.uuid
end
def field_class
OpenProject::InplaceEdit::FieldRegistry.fetch(attribute)
end
def edit_field_component(form)
field_class.new(
form:,
attribute:,
model:,
**@system_arguments
)
end
def display_field_class
if field_class.respond_to?(:display_class)
field_class.display_class
else
InplaceEditFields::DisplayFields::DisplayFieldComponent
end
end
def display_field_component
return nil if display_field_class.nil?
display_field_class.new(model:, attribute:, writable: writable?, **@system_arguments)
end
def wrapper_key
model_class = @model.class.name.parameterize(separator: "_")
"op-inplace-edit-field--#{model_class}-#{model.id}--#{attribute.name}--#{@system_arguments[:id]}"
end
def wrapper_test_selector
"op-inplace-edit-field"
end
private
def writable?
return @writable if defined?(@writable)
contract_class = OpenProject::InplaceEdit::UpdateRegistry.fetch_contract(model)
@writable =
if contract_class.present?
contract_class.new(model, User.current).writable?(attribute)
else
false
end
end
end
end
end
@@ -0,0 +1,89 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject
module Common
module InplaceEditFields
module DisplayFields
class DisplayFieldComponent < ViewComponent::Base
include OpenProject::TextFormatting
attr_reader :model, :attribute, :writable
def initialize(model:, attribute:, writable:, **system_arguments)
super()
@model = model
@attribute = attribute
@writable = writable
@system_arguments = system_arguments
end
def render_display_value
value = model.public_send(attribute)
if value.present?
format_text(value)
else
""
end
end
def display_field_arguments
@display_field_arguments ||= {
classes: "op-inplace-edit--display-field #{'op-inplace-edit--display-field_editable' if writable}",
data: {
controller: "inplace-edit",
inplace_edit_url_value: edit_url,
action: writable ? "click->inplace-edit#request" : ""
}
}
end
def call
render(Primer::BaseComponent.new(tag: :div, **display_field_arguments)) do
render_display_value
end
end
private
def edit_url
inplace_edit_field_edit_path(
model: model.class.name,
id: model.id,
attribute:,
system_arguments_json: @system_arguments.to_json
)
end
end
end
end
end
end
@@ -0,0 +1,16 @@
.op-inplace-edit
&--display-field
&_editable
margin-left: -9px !important // cancel out 8px padding + 1px border
margin-right: -9px !important // cancel out 8px padding + 1px border
padding: var(--base-size-8)
width: calc(100% + 18px) !important
border: 1px solid transparent
border-radius: var(--borderRadius-medium)
&:hover, &:focus
border-color: var(--borderColor-default)
box-shadow: var(--shadow-inset)
&:not(&_editable)
cursor: not-allowed
@@ -0,0 +1,50 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject
module Common
module InplaceEditFields
module DisplayFields
class RichTextAreaComponent < DisplayFieldComponent
attr_reader :model, :attribute, :writable
def call
render(Primer::BaseComponent.new(tag: :div, **display_field_arguments)) do
render(Primer::BaseComponent.new(tag: :div,
classes: "op-uc-container op-uc-container_reduced-headings -multiline")) do
render_display_value
end
end
end
end
end
end
end
end
@@ -0,0 +1 @@
@import "display_fields/display_fields_component"
@@ -0,0 +1,77 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject
module Common
module InplaceEditFields
class RichTextAreaComponent < ViewComponent::Base
attr_reader :form, :attribute, :model
def self.display_class
DisplayFields::RichTextAreaComponent
end
def initialize(form:, attribute:, model:, **system_arguments)
super()
@form = form
@attribute = attribute
@model = model
@system_arguments = system_arguments
@system_arguments[:classes] = class_names(
@system_arguments[:classes],
"op-inplace-edit-field--text-area"
)
@system_arguments[:label] ||= model.class.human_attribute_name(attribute)
@system_arguments[:rich_text_options] ||= {}
@system_arguments[:rich_text_options][:primerized] = true
end
def call
form.rich_text_area(name: attribute, **@system_arguments)
form.group(layout: :horizontal, justify_content: :flex_end) do |button_group|
button_group.submit(name: :reset,
type: :submit,
label: I18n.t(:button_cancel),
scheme: :default,
formaction: inplace_edit_field_reset_path(model: model.class.name, id: model.id, attribute:),
formmethod: :get,
test_selector: "op-inplace-edit-field--textarea-cancel")
button_group.submit(name: :submit,
label: I18n.t(:button_save),
scheme: :primary,
test_selector: "op-inplace-edit-field--textarea-save")
end
end
end
end
end
end
@@ -0,0 +1,71 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject
module Common
module InplaceEditFields
class TextInputComponent < ViewComponent::Base
attr_reader :form, :attribute, :model
def self.display_class
DisplayFields::DisplayFieldComponent
end
def initialize(form:, attribute:, model:, **system_arguments)
super()
@form = form
@attribute = attribute
@model = model
@system_arguments = system_arguments
@system_arguments[:label] ||= model.class.human_attribute_name(attribute)
end
def call
form.text_field name: attribute,
data: { controller: "inplace-edit",
inplace_edit_url_value: reset_url,
action: "keydown.esc->inplace-edit#request" },
**@system_arguments
end
private
def reset_url
inplace_edit_field_reset_path(
model: model.class.name,
id: model.id,
attribute:,
system_arguments_json: @system_arguments.to_json
)
end
end
end
end
end
+6 -6
View File
@@ -83,7 +83,7 @@ module Projects
end
def custom_field_column(column) # rubocop:disable Metrics/AbcSize
return nil unless user_can_view_project?
return nil unless user_can_view_project_attributes?
cf = column.custom_field
custom_value = project.formatted_custom_value_for(cf)
@@ -93,7 +93,7 @@ module Projects
"dialog-#{project.id}-cf-#{cf.id}",
cf.name,
custom_value,
formatted: true
format: false # already formatted
)
elsif custom_value.is_a?(Array)
safe_join(Array(custom_value).compact_blank, ", ")
@@ -196,7 +196,7 @@ module Projects
end
def project_status
return nil unless user_can_view_project?
return nil unless user_can_view_project_attributes?
content = "".html_safe
@@ -212,7 +212,7 @@ module Projects
end
def status_explanation
return nil unless user_can_view_project?
return nil unless user_can_view_project_attributes?
if project.status_explanation.present? && project.status_explanation
render OpenProject::Common::AttributeComponent.new("dialog-#{project.id}-status-explanation",
@@ -222,7 +222,7 @@ module Projects
end
def description
return nil unless user_can_view_project?
return nil unless user_can_view_project_attributes?
if project.description.present?
render OpenProject::Common::AttributeComponent.new("dialog-#{project.id}-description",
@@ -436,7 +436,7 @@ module Projects
end
end
def user_can_view_project?
def user_can_view_project_attributes?
User.current.allowed_in_project?(:view_project_attributes, project)
end
@@ -28,9 +28,9 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%=
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title { @custom_field.attribute_in_database("name") }
header.with_title { page_title }
header.with_description { t("settings.project_attributes.edit.description") } unless hide_description?
header.with_breadcrumbs(breadcrumbs_items)
header.with_breadcrumbs(breadcrumbs_items, selected_item_font_weight: :normal)
helpers.render_tab_header_nav(header, tabs, test_selector: :project_attribute_detail_header)
end
@@ -78,11 +78,19 @@ module Settings
tabs
end
def page_title
concat @custom_field.attribute_in_database("name")
concat render(Primer::Beta::Text.new(color: :muted)) { " (#{helpers.label_for_custom_field_format(@custom_field.field_format)})" }
end
def breadcrumbs_items
[{ href: admin_index_path, text: t("label_administration") },
{ href: admin_settings_project_custom_fields_path, text: t("label_project_plural") },
{ href: admin_settings_project_custom_fields_path, text: t("settings.project_attributes.heading") },
@custom_field.attribute_in_database("name")]
[
{ href: admin_index_path, text: t("label_administration") },
{ href: admin_settings_project_custom_fields_path, text: t("label_project_plural") },
{ href: admin_settings_project_custom_fields_path, text: t("settings.project_attributes.heading") },
helpers.nested_breadcrumb_element(helpers.label_for_custom_field_format(@custom_field.field_format),
@custom_field.attribute_in_database("name"))
]
end
def hide_description?
@@ -29,7 +29,7 @@ See COPYRIGHT and LICENSE files for more details.
<%=
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title { t("settings.project_attributes.new.heading") }
header.with_title { page_title }
header.with_description { t("settings.project_attributes.new.description") } unless hide_description?
header.with_breadcrumbs(breadcrumb_items, selected_item_font_weight: :normal)
end
@@ -31,6 +31,11 @@
module Settings
module ProjectCustomFields
class NewFormHeaderComponent < ApplicationComponent
def page_title
concat t("settings.project_attributes.new.heading")
concat render(Primer::Beta::Text.new(color: :muted)) { " (#{helpers.label_for_custom_field_format(model.field_format)})" }
end
def breadcrumb_items
[
{ href: admin_index_path, text: t("label_administration") },
@@ -29,7 +29,7 @@
#++
module Shares
class PermissionButtonComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
class PermissionButtonComponent < ApplicationComponent
include ApplicationHelper
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable
+1 -1
View File
@@ -34,7 +34,7 @@ class Users::HoverCardComponent < ApplicationComponent
def initialize(id:)
super
@user = User.find_by(id:)
@user = User.visible.find_by(id:)
end
def render?
+19 -16
View File
@@ -31,14 +31,14 @@
module Projects
class UpdateContract < BaseContract
def writable_attributes
if allow_project_attributes_only
if allow_project_attributes_only?
with_available_custom_fields_only(super)
elsif allow_edit_attributes_only
elsif allow_edit_attributes_only?
without_custom_fields(super)
elsif allow_all_attributes
elsif allow_all_attributes?
# When all attributes are updated (API-only case), allow writing to all available custom
# fields (including disabled ones) to maintain backward compatibility with the API.
with_all_available_custom_fields_only(super)
with_all_available_custom_fields(super)
else
[]
end
@@ -46,28 +46,31 @@ module Projects
private
def project_attributes_only = options[:project_attributes_only].present?
def project_attributes_only? = options[:project_attributes_only].present?
def edit_project = user.allowed_in_project?(:edit_project, model)
def allow_edit_project? = user.allowed_in_project?(:edit_project, model)
def edit_project_attributes = user.allowed_in_project?(:edit_project_attributes, model)
def allow_edit_project_attributes? = user.allowed_in_project?(:edit_project_attributes, model)
def allow_edit_attributes_only = edit_project && !project_attributes_only && !edit_project_attributes
def allow_project_attributes_only
edit_project_attributes && (project_attributes_only || !edit_project)
def allow_edit_attributes_only?
allow_edit_project? && !project_attributes_only? && !allow_edit_project_attributes?
end
def allow_all_attributes
(edit_project && edit_project_attributes && !project_attributes_only) ||
(changed_by_user == ["active"]) # Allow archiving, permission checked in manage_permission
def allow_project_attributes_only?
allow_edit_project_attributes? && (project_attributes_only? || !allow_edit_project?)
end
def allow_all_attributes?
return true if allow_edit_project? && allow_edit_project_attributes? && !project_attributes_only?
changed_by_user == ["active"] # Allow archiving, permission checked in manage_permission
end
def without_custom_fields(changes) = changes.grep_v(/^custom_field_/)
def with_available_custom_fields_only(changes) = changes & available_custom_fields.map(&:attribute_name)
def with_all_available_custom_fields_only(changes)
def with_all_available_custom_fields(changes)
allowed_attributes = changes.grep_v(/^custom_field_/)
allowed_attributes += changes & all_available_custom_fields.map(&:attribute_name)
allowed_attributes
@@ -76,7 +79,7 @@ module Projects
def manage_permission
if changed_by_user == ["active"]
:archive_project
elsif project_attributes_only
elsif project_attributes_only?
:edit_project_attributes
else
# if "active" is changed, :archive_project permission will also be
@@ -131,6 +131,14 @@ module WorkPackages
attribute :budget
validates :subject,
presence: true,
unless: -> { model.type&.replacement_pattern_defined_for?(:subject) }
validates :subject, length: { maximum: 255 }
# TODO: add validation, check permission (#71253)
attribute :sprint_id
validates :due_date,
date: { after_or_equal_to: :start_date,
message: :greater_than_or_equal_to_start_date,
@@ -239,6 +247,16 @@ module WorkPackages
def valid?(context = :saving_custom_fields) = super
def writable_attributes
attributes = super
unless auto_generated_attributes_writable?
attributes -= auto_generated_attribute_names
end
attributes
end
private
def validate_after_soonest_start(date_attribute)
@@ -680,5 +698,11 @@ module WorkPackages
def leaf_or_manually_scheduled?
model.leaf? || model.schedule_manually?
end
def auto_generated_attributes_writable? = false
def auto_generated_attribute_names
(model.type && model.type.enabled_patterns&.keys&.map(&:to_s)) || []
end
end
end
@@ -51,5 +51,12 @@ module WorkPackages
# might not be active in the project yet. But when it is activated later,
# the value should then be present.
def validate_phase_active_in_project; end
private
# Auto-generated attributes are ok to be writable. The input does not come from the
# user so there is no need to run into a "read only error".
# The actual values will be regenerated after saving.
def auto_generated_attributes_writable? = true
end
end
@@ -32,6 +32,8 @@ module WorkPackages
class CreateNoteContract < ::ModelContract
def self.model = WorkPackage
def validate_model? = false
attribute :journal_notes do
errors.add(:journal_notes, :error_unauthorized) unless adding_notes_allowed?
errors.add(:journal_notes, :blank) if model.journal_notes.blank?
@@ -34,10 +34,8 @@ class Admin::CustomFields::CustomFieldProjectsController < ApplicationController
layout "admin"
model_object CustomField
before_action :require_admin
before_action :find_model_object
before_action :find_custom_field
before_action :available_custom_fields_projects_query, only: %i[index destroy]
before_action :initialize_custom_field_project, only: :new
@@ -99,14 +97,13 @@ class Admin::CustomFields::CustomFieldProjectsController < ApplicationController
)
end
def find_model_object(object_id = :custom_field_id)
super
@custom_field = @object
def find_custom_field
@custom_field = CustomField.find(params[:custom_field_id])
end
def find_projects_to_activate_for_custom_field
if (project_ids = params.to_unsafe_h[:custom_fields_project][:project_ids]).present?
@projects = Project.find(project_ids)
@projects = Project.visible.find(project_ids)
else
initialize_custom_field_project
@custom_field_project.errors.add(:project_ids, :blank)
@@ -36,9 +36,10 @@ module Admin
include Dry::Monads[:result]
layout :admin_or_frame_layout
model_object CustomField
before_action :require_admin, :find_model_object, :find_active_item
before_action :require_admin
before_action :find_custom_field
before_action :find_active_item
# See https://github.com/hotwired/turbo-rails?tab=readme-ov-file#a-note-on-custom-layouts
def admin_or_frame_layout
@@ -225,11 +226,6 @@ module Admin
end
end
def find_model_object
@object = find_custom_field
@custom_field = @object
end
def find_custom_field
raise NotImplementedError, "SubclassResponsibility"
end
@@ -238,7 +234,7 @@ module Admin
@active_item = if params[:id].present?
CustomField::Hierarchy::Item.including_children.find(params[:id])
else
@object.hierarchy_root
@custom_field.hierarchy_root
end
end
end
@@ -37,7 +37,7 @@ module Admin
private
def find_custom_field
CustomField.hierarchy_root_and_children.find(params[:custom_field_id])
@custom_field = CustomField.hierarchy_root_and_children.find(params[:custom_field_id])
end
end
end
@@ -38,7 +38,7 @@ module Admin
private
def find_custom_field
CustomField.hierarchy_root_and_children.find(params[:project_custom_field_id])
@custom_field = CustomField.hierarchy_root_and_children.find(params[:project_custom_field_id])
end
end
end
+3 -65
View File
@@ -34,7 +34,6 @@ require "cgi"
require "doorkeeper/dashboard_helper"
class ApplicationController < ActionController::Base
class_attribute :_model_object
class_attribute :_model_scope
class_attribute :accept_key_auth_actions
@@ -246,18 +245,18 @@ class ApplicationController < ActionController::Base
# Find project of id params[:id]
# Note: find() is Project.friendly.find()
def find_project
@project = Project.find(params[:id])
@project = Project.visible.find(params[:id])
end
# Find project of id params[:project_id]
# Note: find() is Project.friendly.find()
def find_project_by_project_id
@project = Project.find(params[:project_id])
@project = Project.visible.find(params[:project_id])
end
# Find project by project_id if given
def find_optional_project
@project = Project.find(params[:project_id]) if params[:project_id].present?
@project = Project.visible.find(params[:project_id]) if params[:project_id].present?
rescue ActiveRecord::RecordNotFound
render_404
end
@@ -269,67 +268,6 @@ class ApplicationController < ActionController::Base
@project = @object.project
end
def find_model_object(object_id = :id)
model = self.class._model_object
if model
@object = model.find(params[object_id])
instance_variable_set(:"@#{controller_name.singularize}", @object) if @object
end
end
def find_model_object_and_project(object_id = :id)
if params[object_id]
model_object = self.class._model_object
instance = model_object.find(params[object_id])
@project = instance.project
instance_variable_set(:"@#{model_object.to_s.underscore}", instance)
else
@project = Project.find(params[:project_id])
end
end
# TODO: this method is right now only suited for controllers of objects that somehow have an association to Project
def find_object_and_scope
model_object = self.class._model_object.find(params[:id]) if params[:id].present?
associations = self.class._model_scope + [Project]
associated = find_belongs_to_chained_objects(associations, model_object)
associated.each do |a|
instance_variable_set("@" + a.class.to_s.downcase, a)
end
end
# this method finds all records that are specified in the associations param
# after the first object is found it traverses the belongs_to chain of that first object
# if a start_object is provided it is taken as the starting point of the traversal
# e.g associations [Message, Board, Project] finds Message by find(:message_id)
# then message.forum and board.project
def find_belongs_to_chained_objects(associations, start_object = nil)
associations.inject([start_object].compact) do |instances, association|
scope_name, scope_association = if association.is_a?(Hash)
[association.keys.first.to_s.downcase, association.values.first]
else
[association.to_s.downcase, association.to_s.downcase]
end
# TODO: Remove this hidden dependency on params
instances << (
if instances.last.nil?
scope_name.camelize.constantize.find(params[:"#{scope_name}_id"])
else
instances.last.send(scope_association.to_sym)
end)
instances
end
end
def self.model_object(model, options = {})
self._model_object = model
self._model_scope = Array(options[:scope]) if options[:scope]
end
# Filter for bulk work package operations
def find_work_packages
@work_packages = WorkPackage.includes(:project)
+6 -9
View File
@@ -30,9 +30,8 @@
class CategoriesController < ApplicationController
menu_item :settings_categories
model_object Category
before_action :find_model_object, except: %i[new create]
before_action :find_project_from_association, except: %i[new create]
before_action :find_category_and_project, except: %i[new create]
before_action :find_project, only: %i[new create]
before_action :authorize
@@ -81,14 +80,12 @@ class CategoriesController < ApplicationController
private
# Wrap ApplicationController's find_model_object method to set
# @category instead of just @category
def find_model_object
super
@category = @object
def find_category_and_project
@category = Category.find(params[:id])
@project = @category.project
end
def find_project
@project = Project.find(params[:project_id])
@project = Project.visible.find(params[:project_id])
end
end
@@ -101,7 +101,7 @@ module Accounts::CurrentUser
end
def current_api_key_user
return unless Setting.rest_api_enabled? && api_request?
return unless Setting.api_tokens_enabled? && api_request?
key = api_key_from_request
@@ -168,7 +168,7 @@ module Accounts::CurrentUser
redirect_to main_app.signin_path(back_url: login_back_url)
end
auth_header = OpenProject::Authentication::WWWAuthenticate.response_header(request_headers: request.headers)
auth_header = OpenProject::Authentication::WWWAuthenticate.response_header
format.any(:xml, :js, :json, :turbo_stream) do
head :unauthorized,
+5 -2
View File
@@ -35,8 +35,7 @@ class CustomActionsController < ApplicationController
redirect_to action: :index
end
self._model_object = CustomAction
before_action :find_model_object, only: %i(edit update destroy)
before_action :find_custom_action, only: %i(edit update destroy)
before_action :pad_params, only: %i(create update)
layout "admin"
@@ -73,6 +72,10 @@ class CustomActionsController < ApplicationController
private
def find_custom_action
@custom_action = CustomAction.find(params[:id])
end
def index_or_render(render_action)
->(call) {
call.on_success do
@@ -34,19 +34,31 @@ class ExternalLinkWarningController < ApplicationController
skip_before_action :check_if_login_required
no_authorization_required! :show
before_action :parse_external_url, only: [:show]
before_action :verify_capture_enabled, only: [:show]
before_action :parse_external_url
before_action :verify_capture_enabled
before_action :optional_require_login
def show; end
private
def login_back_url_params
params.permit(:url)
end
def verify_capture_enabled
unless capture_enabled?
redirect_to @external_url, allow_other_host: true, status: :see_other
end
end
def optional_require_login
return unless Setting.capture_external_links?
return unless Setting.capture_external_links_require_login?
require_login
end
def capture_enabled?
Setting.capture_external_links? && EnterpriseToken.allows_to?(:capture_external_links)
end
+20 -20
View File
@@ -30,10 +30,12 @@
class ForumsController < ApplicationController
default_search_scope :messages
before_action :find_project_by_project_id,
:authorize
before_action :find_project_by_project_id
before_action :new_forum, only: %i[new create]
before_action :find_forum, only: %i[show edit update move destroy]
before_action :authorize
accept_key_auth :show
include SortHelper
@@ -47,7 +49,7 @@ class ForumsController < ApplicationController
:forums
end
def show
def show # rubocop:disable Metrics/AbcSize
sort_init "updated_at", "desc"
sort_update "created_at" => "#{Message.table_name}.created_at",
"replies" => "#{Message.table_name}.replies_count",
@@ -59,11 +61,11 @@ class ForumsController < ApplicationController
@message = Message.new
render action: "show", layout: !request.xhr?
end
format.json do
set_topics
render template: "messages/index"
end
# The JSON template does not exist anymore, this never rendered
# format.json do
# set_topics
# render template: "messages/index"
# end
format.atom do
@messages = @forum
.messages
@@ -92,7 +94,7 @@ class ForumsController < ApplicationController
def create
if @forum.save
flash[:notice] = I18n.t(:notice_successful_create)
redirect_to action: "index"
redirect_to project_forums_path(@project)
else
render :new
end
@@ -101,26 +103,24 @@ class ForumsController < ApplicationController
def update
if @forum.update(permitted_params.forum)
flash[:notice] = I18n.t(:notice_successful_update)
redirect_to action: "index"
redirect_to project_forums_path(@project)
else
render :edit
render :edit, status: :unprocessable_entity
end
end
def move
if @forum.update(permitted_params.forum_move)
flash[:notice] = t(:notice_successful_update)
else
flash.now[:error] = t("forum_could_not_be_saved")
render action: :edit, status: :unprocessable_entity
end
redirect_to action: "index"
@forum.update!(permitted_params.forum_move)
flash[:notice] = t(:notice_successful_update)
redirect_to project_forums_path(@project)
end
def destroy
@forum.destroy
@forum.destroy!
flash[:notice] = I18n.t(:notice_successful_delete)
redirect_to action: "index", status: :see_other
redirect_to project_forums_path(@project)
end
private
+1 -1
View File
@@ -150,7 +150,7 @@ class GroupsController < ApplicationController
protected
def find_group
@group = Group.find(params[:id])
@group = Group.visible.find(params[:id])
end
def group_members
@@ -0,0 +1,131 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class InplaceEditFieldsController < ApplicationController
include OpTurbo::ComponentStream
before_action :find_model
before_action :set_attribute
no_authorization_required! :edit, :update, :reset
def edit
replace_via_turbo_stream(
component: component(enforce_edit_mode: true),
status: :ok
)
respond_with_turbo_streams
end
def update
handler = OpenProject::InplaceEdit::UpdateRegistry.fetch_handler(@model)
if handler.present?
success = handler.call(
model: @model,
params: permitted_params,
user: current_user
)
else
raise ArgumentError, "Missing update handler for #{@model}"
end
if success
render_success_flash_message_via_turbo_stream(
message: I18n.t(:notice_successful_update)
)
end
replace_via_turbo_stream(
component: component(enforce_edit_mode: !success),
status: success ? :ok : :unprocessable_entity
)
respond_with_turbo_streams
rescue ArgumentError
head :not_found
end
def reset
replace_via_turbo_stream(component:)
respond_with_turbo_streams
end
private
def find_model
model_class = resolve_model_class(params[:model])
@model = model_class.visible.find(params[:id])
rescue ActiveRecord::RecordNotFound, ArgumentError
head :not_found
end
def resolve_model_class(model_param)
return nil if model_param.blank?
model_class =
OpenProject::InplaceEdit::UpdateRegistry.resolve_model_class(model_param)
unless model_class &&
model_class < ApplicationRecord &&
model_class.respond_to?(:visible)
raise ArgumentError, "Unsupported model for inplace edit"
end
model_class
end
def set_attribute
@attribute = params[:attribute].to_sym
end
def permitted_params
params
.expect(@model.model_name.param_key => [@attribute])
end
def component(enforce_edit_mode: false)
OpenProject::Common::InplaceEditFieldComponent.new(
model: @model,
attribute: @attribute,
enforce_edit_mode:,
**system_arguments.to_h.symbolize_keys
)
end
def system_arguments
arguments = params[:system_arguments_json].presence || params.to_unsafe_h
.values
.filter_map { |v| v["system_arguments_json"] }
.first
arguments.nil? ? {} : JSON.parse(arguments)
end
end
@@ -31,13 +31,13 @@
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 :find_ldap_auth_source, only: %i(edit update destroy)
before_action :prevent_editing_when_seeded, only: %i(update)
def index
@@ -101,6 +101,10 @@ class LdapAuthSourcesController < ApplicationController
protected
def find_ldap_auth_source
@ldap_auth_source = LdapAuthSource.find(params[:id])
end
def prevent_editing_when_seeded
if @ldap_auth_source.seeded_from_env?
flash[:warning] = I18n.t(:label_seeded_from_env_warning)
+13 -9
View File
@@ -31,16 +31,15 @@
class MembersController < ApplicationController
include MemberHelper
model_object Member
before_action :find_model_object_and_project, except: %i[autocomplete_for_member destroy_by_principal]
before_action :find_project_by_project_id, only: %i[autocomplete_for_member destroy_by_principal]
before_action :find_project_by_project_id
before_action :find_member, except: %i[index create autocomplete_for_member destroy_by_principal]
before_action :authorize
def index
set_index_data!
end
def create
def create # rubocop:disable Metrics/AbcSize
overall_result = []
find_or_create_users(send_notification: true) do |member_params|
@@ -85,8 +84,8 @@ class MembersController < ApplicationController
per_page: params[:per_page])
end
def destroy_by_principal
principal = Principal.find(params[:principal_id])
def destroy_by_principal # rubocop:disable Metrics/AbcSize
principal = Principal.visible.find(params[:principal_id])
service_call = Members::DeleteByPrincipalService
.new(user: current_user, project: @project, principal:)
@@ -118,7 +117,11 @@ class MembersController < ApplicationController
private
def authorize_for(controller, action)
def find_member
@member = @project.members.visible.find(params[:id])
end
def authorize_for?(controller, action)
current_user.allowed_in_project?({ controller:, action: }, @project)
end
@@ -152,8 +155,8 @@ class MembersController < ApplicationController
{
project: @project,
available_roles: roles,
authorize_update: authorize_for("members", :update),
authorize_delete: authorize_for("members", :destroy),
authorize_update: authorize_for?("members", :update),
authorize_delete: authorize_for?("members", :destroy),
authorize_work_package_shares_view: current_user.allowed_in_project?(:view_shared_work_packages, @project),
authorize_work_package_shares_delete: current_user.allowed_in_project?(:share_work_packages, @project),
authorize_manage_user: current_user.allowed_globally?(:manage_user),
@@ -205,6 +208,7 @@ class MembersController < ApplicationController
def possible_members(criteria, limit, type: nil)
Principal
.visible
.possible_member(@project, type:)
.like(criteria, email: user_allowed_to_view_emails?)
.limit(limit)
+16 -7
View File
@@ -31,8 +31,8 @@
class MessagesController < ApplicationController
menu_item :forums
default_search_scope :messages
model_object Message, scope: Forum
before_action :find_object_and_scope
before_action :find_project_and_forum
before_action :find_message, only: %i[show edit update destroy reply quote]
before_action :authorize, except: %i[edit update destroy]
# Checked inside the method.
no_authorization_required! :edit, :update, :destroy
@@ -89,7 +89,7 @@ class MessagesController < ApplicationController
if call.success?
call_hook(:controller_messages_new_after_save, params:, message: @message)
redirect_to topic_path(@message)
redirect_to project_forum_topic_path(@project, @forum, @message)
else
render action: :new, status: :unprocessable_entity
end
@@ -105,7 +105,7 @@ class MessagesController < ApplicationController
if call.success?
call_hook(:controller_messages_reply_after_save, params:, message: @reply)
end
redirect_to topic_path(@topic, r: @reply)
redirect_to project_forum_topic_path(@project, @forum, @topic, r: @reply)
end
# Edit a message
@@ -118,7 +118,7 @@ class MessagesController < ApplicationController
if call.success?
flash[:notice] = t(:notice_successful_update)
@message.reload
redirect_to topic_path(@message.root, r: @message.parent_id && @message.id)
redirect_to project_forum_topic_path(@project, @forum, @message.root, r: @message.parent_id && @message.id)
else
render action: :edit, status: :unprocessable_entity
end
@@ -132,9 +132,9 @@ class MessagesController < ApplicationController
@message.destroy
flash[:notice] = t(:notice_successful_delete)
redirect_target = if @message.parent.nil?
{ controller: "/forums", action: "show", project_id: @project, id: @forum }
project_forum_path(@project, @forum)
else
{ action: "show", id: @message.parent, r: @message }
project_forum_topic_path(@project, @forum, @message.parent, r: @message)
end
redirect_to redirect_target, status: :see_other
@@ -157,6 +157,15 @@ class MessagesController < ApplicationController
private
def find_project_and_forum
@project = Project.visible.find(params[:project_id])
@forum = @project.forums.find(params[:forum_id])
end
def find_message
@message = @forum.messages.find(params[:id])
end
def update_message(message)
Messages::UpdateService
.new(user: current_user,
@@ -172,7 +172,7 @@ module My
helper_method :has_tokens?
def has_tokens?
Setting.feeds_enabled? || Setting.rest_api_enabled? || current_user.ical_tokens.any?
Setting.feeds_enabled? || Setting.api_tokens_enabled? || current_user.ical_tokens.any?
end
def set_api_token
+17 -5
View File
@@ -30,8 +30,9 @@
class News::CommentsController < ApplicationController
default_search_scope :news
model_object Comment, scope: [News => :commented]
before_action :find_object_and_scope
before_action :find_news_and_project
before_action :find_comment, only: [:destroy]
before_action :authorize
def create
@@ -41,11 +42,22 @@ class News::CommentsController < ApplicationController
flash[:notice] = I18n.t(:label_comment_added)
end
redirect_to news_path(@news), status: :see_other
redirect_to project_news_path(@project, @news), status: :see_other
end
def destroy
@comment.destroy
redirect_to news_path(@news), status: :see_other
@comment.destroy!
redirect_to project_news_path(@project, @news), status: :see_other
end
private
def find_comment
@comment = @news.comments.find(params[:id])
end
def find_news_and_project
@project = Project.visible.find(params[:project_id])
@news = @project.news.visible.find(params[:news_id])
end
end
+14 -14
View File
@@ -34,17 +34,16 @@ class NewsController < ApplicationController
default_search_scope :news
before_action :load_and_authorize_in_optional_project
before_action :find_news_object, except: %i[new create index]
before_action :find_project_from_association, except: %i[new create index]
before_action :find_project, only: %i[new create]
before_action :authorize, except: [:index]
before_action :load_and_authorize_in_optional_project, only: [:index]
before_action :authorize
accept_key_auth :index
def index
scope = @project ? @project.news : News.all
def index # rubocop:disable Metrics/AbcSize
scope = @project ? @project.news : News.visible
@newss = scope.merge(News.latest_for(current_user, count: 0))
@news = scope.merge(News.latest_for(current_user, count: 0))
.page(page_param)
.per_page(per_page_param)
@@ -53,7 +52,7 @@ class NewsController < ApplicationController
render locals: { menu_name: project_or_global_menu }
end
format.atom do
render_feed(@newss,
render_feed(@news,
title: (@project ? @project.name : Setting.app_title) + ": #{I18n.t(:label_news_plural)}")
end
end
@@ -95,7 +94,7 @@ class NewsController < ApplicationController
if call.success?
flash[:notice] = I18n.t(:notice_successful_update)
redirect_to action: "show", id: @news
redirect_to project_news_path(@project, @news)
else
@news = call.result
render action: :edit, status: :unprocessable_entity
@@ -119,10 +118,11 @@ class NewsController < ApplicationController
private
def find_news_object
@news = @object = News.find(params[:id].to_i)
end
def find_project
@project = Project.find(params[:project_id])
if @project
@news = @project.news.visible.find(params[:id].to_i)
else
@news = News.visible.find(params[:id].to_i)
@project = @news.project
end
end
end
@@ -30,13 +30,14 @@
class PlaceholderUsers::MembershipsController < ApplicationController
include IndividualPrincipals::MembershipControllerMethods
layout "admin"
before_action :authorize_global
before_action :find_individual_principal
def find_individual_principal
@individual_principal = PlaceholderUser.find(params[:placeholder_user_id])
@individual_principal = PlaceholderUser.visible.find(params[:placeholder_user_id])
end
def redirected_to_tab(_membership)
@@ -111,7 +111,7 @@ class PlaceholderUsersController < ApplicationController
respond_to do |format|
format.html do
flash[:notice] = I18n.t(:notice_successful_update)
redirect_back(fallback_location: edit_placeholder_user_path(@placeholder_user))
redirect_back_or_to(edit_placeholder_user_path(@placeholder_user))
end
end
else
@@ -146,7 +146,7 @@ class PlaceholderUsersController < ApplicationController
private
def find_placeholder_user
@placeholder_user = PlaceholderUser.find(params[:id])
@placeholder_user = PlaceholderUser.visible.find(params[:id])
end
protected
+10 -6
View File
@@ -31,7 +31,7 @@
class Projects::ArchiveController < ApplicationController
include OpTurbo::ComponentStream
before_action :find_project_by_project_id
before_action :find_project_including_archived
before_action :authorize, only: %i[create dialog]
before_action :require_admin, only: [:destroy]
@@ -49,17 +49,21 @@ class Projects::ArchiveController < ApplicationController
private
def find_project_including_archived
# The visible scope filters out archived projects, but here we want to explicitly unarchive them.
# The contracts do proper permission checks, so we can skip the visible scope here.
@project = Project.find(params[:project_id])
end
def change_status_action(status)
service_call = change_status(status)
if service_call.success?
redirect_to(projects_path, status: :see_other)
else
if !service_call.success?
flash[:error] = t(:"error_can_not_#{status}_project",
errors: service_call.errors.full_messages.join(", "))
redirect_back fallback_location: projects_path,
status: :see_other
end
redirect_to(projects_path, status: :see_other)
end
def change_status(status)
+8 -1
View File
@@ -34,7 +34,8 @@ class ProjectsController < ApplicationController
menu_item :overview
menu_item :roadmap, only: :roadmap
before_action :find_project, except: %i[index new create]
before_action :find_project, except: %i[index new create destroy destroy_info]
before_action :find_project_including_archived, only: %i[destroy destroy_info]
before_action :load_query_or_deny_access, only: %i[index]
before_action :authorize,
only: %i[copy_form copy deactivate_work_package_attachments export_project_initiation_pdf]
@@ -181,6 +182,12 @@ class ProjectsController < ApplicationController
private
def find_project_including_archived
# The actions that use this method are only accessible to admins, so we can show them archived projects as well and
# can skip the visible scope here.
@project = Project.find(params[:id])
end
def from_template? = @template.present?
def new_blank
+1 -1
View File
@@ -57,7 +57,7 @@ class SharesController < ApplicationController
visible_shares_before_adding = sharing_strategy.shares.present?
find_or_create_users(send_notification: send_notification?) do |member_params|
user = User.find_by(id: member_params[:user_id])
user = User.visible.find_by(id: member_params[:user_id])
if user.present? && (user.locked? || user.deleted?)
@errors.add(:base, I18n.t("sharing.warning_locked_user", user: user.name))
else
@@ -30,13 +30,14 @@
class Users::MembershipsController < ApplicationController
include IndividualPrincipals::MembershipControllerMethods
layout "admin"
before_action :authorize_global
before_action :find_individual_principal
def find_individual_principal
@individual_principal = User.find(params[:user_id])
@individual_principal = User.visible.find(params[:user_id])
end
def redirected_to_tab(membership)
+7 -4
View File
@@ -32,9 +32,7 @@ class VersionsController < ApplicationController
menu_item :roadmap, only: %i(index show)
menu_item :settings_versions
model_object Version
before_action :find_model_object, except: %i[index new create close_completed]
before_action :find_project_from_association, except: %i[index new create close_completed]
before_action :find_version, except: %i[index new create close_completed]
before_action :find_project, only: %i[index new create close_completed]
before_action :authorize
@@ -114,6 +112,11 @@ class VersionsController < ApplicationController
private
def find_version
@version = Version.visible.find(params[:id])
@project = @version.project
end
def archived_project_mesage
if current_user.admin?
ApplicationController.helpers.sanitize(
@@ -135,7 +138,7 @@ class VersionsController < ApplicationController
end
def find_project
@project = Project.find(params[:project_id])
@project = Project.visible.find(params[:project_id])
end
def retrieve_selected_type_ids(selectable_types, default_types = nil)
+1 -1
View File
@@ -370,7 +370,7 @@ class WikiController < ApplicationController
end
def find_wiki
@project = Project.find(params[:project_id])
@project = Project.visible.find(params[:project_id])
@wiki = @project.wiki
render_404 unless @wiki
end
+17 -12
View File
@@ -37,7 +37,7 @@ class WikiMenuItemsController < ApplicationController
next controller.wiki_menu_item.menu_identifier if controller.wiki_menu_item.try(:persisted?)
project = controller.instance_variable_get(:@project)
if (page = WikiPage.find_by(wiki_id: project.wiki.id, slug: controller.params[:id]))
if (page = project.wiki.pages.find_by(id: controller.params[:id]))
default_menu_item(controller, page)
end
end
@@ -45,7 +45,8 @@ class WikiMenuItemsController < ApplicationController
current_menu_item :select_main_menu_item do |controller|
next controller.wiki_menu_item.menu_identifier if controller.wiki_menu_item.try(:persisted?)
if (page = WikiPage.find_by(id: controller.params[:id]))
project = controller.instance_variable_get(:@project)
if (page = project.wiki.pages.find_by(id: controller.params[:id]))
default_menu_item(controller, page)
end
end
@@ -114,7 +115,7 @@ class WikiMenuItemsController < ApplicationController
end
def select_main_menu_item
@page = WikiPage.find params[:id]
@page = @project.wiki.pages.find params[:id]
@possible_wiki_pages = @project
.wiki
.pages
@@ -126,12 +127,16 @@ class WikiMenuItemsController < ApplicationController
end
end
def replace_main_menu_item
current_page = WikiPage.find params[:id]
def replace_main_menu_item # rubocop:disable Metrics/AbcSize
current_page = @project.wiki.pages.find(params[:id])
if (current_menu_item = current_page.menu_item) && (page = WikiPage.find_by(id: params[:wiki_page][:id])) && current_menu_item != page.menu_item
create_main_menu_item_for_wiki_page(page, current_menu_item.options)
current_menu_item.destroy
if current_menu_item = current_page.menu_item
page = @project.wiki.pages.find(params[:wiki_page][:id])
if page && current_menu_item != page.menu_item
create_main_menu_item_for_wiki_page(page, current_menu_item.options)
current_menu_item.destroy!
end
end
redirect_to action: :edit, id: current_page
@@ -140,11 +145,11 @@ class WikiMenuItemsController < ApplicationController
private
def wiki_menu_item_params
@wiki_menu_item_params ||= params.require(:menu_items_wiki_menu_item).permit(:name, :title, :navigatable_id, :parent_id,
:setting, :new_wiki_page, :index_page)
@wiki_menu_item_params ||= params.expect(menu_items_wiki_menu_item: %i[name title navigatable_id parent_id
setting new_wiki_page index_page])
end
def get_data_from_params(params)
def get_data_from_params(params) # rubocop:disable Metrics/AbcSize
wiki = @project.wiki
@page = wiki.find_page(params[:id])
@@ -188,6 +193,6 @@ class WikiMenuItemsController < ApplicationController
end
menu_item.options = options
menu_item.save
menu_item.save!
end
end
@@ -65,7 +65,7 @@ class WorkPackageHierarchyRelationsController < ApplicationController
end
def destroy
related = WorkPackage.find(params[:id])
related = WorkPackage.visible.find(params[:id])
service_result =
if related.parent_id == @work_package.id
set_relation(child: related, parent: nil)
@@ -101,7 +101,7 @@ class WorkPackageHierarchyRelationsController < ApplicationController
def related_work_package
@related_work_package ||=
if params[:work_package][:id].present?
WorkPackage.find(params[:work_package][:id])
WorkPackage.visible.find(params[:work_package][:id])
else
WorkPackage.new
end
@@ -139,7 +139,7 @@ class WorkPackageHierarchyRelationsController < ApplicationController
end
def set_work_package
@work_package = WorkPackage.find(params[:work_package_id])
@work_package = WorkPackage.visible.find(params[:work_package_id])
@project = @work_package.project
end
@@ -127,11 +127,11 @@ class WorkPackageRelationsController < ApplicationController
end
def set_work_package
@work_package = WorkPackage.find(params[:work_package_id])
@work_package = WorkPackage.visible.find(params[:work_package_id])
end
def set_relation
@relation = @work_package.relations.find(params[:id])
@relation = @work_package.relations.visible.find(params[:id])
end
def create_relation_params
@@ -51,7 +51,7 @@ class WorkPackageRelationsTabController < ApplicationController
private
def set_work_package
@work_package = WorkPackage.find(params[:work_package_id])
@work_package = WorkPackage.visible.find(params[:work_package_id])
@project = @work_package.project # required for authorization via before_action
end
end
@@ -35,7 +35,6 @@ class WorkPackages::ActivitiesTabController < ApplicationController
include WorkPackages::ActivitiesTab::StimulusControllers
before_action :find_work_package
before_action :find_project
before_action :find_journal, only: %i[emoji_actions item_actions edit cancel_edit update toggle_reaction]
before_action :set_filter
before_action :authorize
@@ -201,43 +200,33 @@ class WorkPackages::ActivitiesTabController < ApplicationController
private
def find_work_package
@work_package = WorkPackage.visible.find(params[:work_package_id])
@project = @work_package.project
rescue ActiveRecord::RecordNotFound
respond_with_error(I18n.t("label_not_found"))
end
def initialize_pagination
@paginator, @paginated_journals = WorkPackages::ActivitiesTab::Paginator
.paginate(@work_package, params.merge(filter: @filter, limit: 20))
end
def respond_with_error(error_message)
respond_to do |format|
# turbo_frame requests (tab is initially rendered and an error occured) are handled below
@turbo_status = :not_found
render_error_flash_message_via_turbo_stream(message: error_message)
respond_to_with_turbo_streams do |format|
format.html do
render(
WorkPackages::ActivitiesTab::ErrorFrameComponent.new(
error_message:
),
WorkPackages::ActivitiesTab::ErrorFrameComponent.new(error_message: error_message),
layout: false,
status: :not_found
)
end
# turbo_stream requests (tab is already rendered and an error occured in subsequent requests) are handled below
format.turbo_stream do
@turbo_status = :not_found
render_error_flash_message_via_turbo_stream(message: error_message)
end
end
end
def find_work_package
@work_package = WorkPackage.find(params[:work_package_id])
rescue ActiveRecord::RecordNotFound
respond_with_error(I18n.t("label_not_found"))
end
def find_project
@project = @work_package.project
rescue ActiveRecord::RecordNotFound
respond_with_error(I18n.t("label_not_found"))
end
def find_journal
@journal = Journal
.with_sequence_version
@@ -48,7 +48,7 @@ module Admin
end
settings_form do |sf|
sf.check_box(name: :rest_api_enabled)
sf.check_box(name: :api_tokens_enabled, caption: I18n.t(:setting_api_tokens_enabled_caption))
sf.text_field(
name: :apiv3_max_page_size,
@@ -36,6 +36,10 @@ module Admin
name: :capture_external_links,
caption: I18n.t(:setting_capture_external_links_text)
)
sf.check_box(
name: :capture_external_links_require_login,
caption: I18n.t(:setting_capture_external_links_require_login_text)
)
end
end
end
@@ -53,7 +53,7 @@ class CustomFields::Inputs::Base::Autocomplete::MultiValueInput < CustomFields::
end
def custom_values
@custom_values ||= @object.custom_values_for_custom_field(id: @custom_field.id)
@custom_values ||= @object.custom_values_for_custom_field(@custom_field)
end
def invalid?
+1 -1
View File
@@ -72,7 +72,7 @@ module CustomFieldsHelper
end
def custom_field_tag_for_bulk_edit(name, custom_field, project = nil) # rubocop:disable Metrics/AbcSize
field_name = "#{name}[custom_field_values][#{custom_field.id}]"
field_name = name.present? ? "#{name}[custom_field_values][#{custom_field.id}]" : "custom_field_values[#{custom_field.id}]"
field_id = "#{name}_custom_field_values_#{custom_field.id}"
field_format = OpenProject::CustomFieldFormat.find_by(name: custom_field.field_format)
+7 -1
View File
@@ -36,6 +36,12 @@ module MessagesHelper
end
def message_url(message)
topic_url(message.root, r: message.id, anchor: "message-#{message.id}")
project_forum_topic_url(
message.forum.project,
message.forum,
message.root,
r: message.id,
anchor: "message-#{message.id}"
)
end
end
@@ -66,11 +66,11 @@ class Activities::MessageActivityProvider < Activities::BaseActivityProvider
end
def event_path(event)
url_helpers.topic_path(*url_helper_parameter(event))
url_helpers.project_forum_topic_path(*url_helper_parameter(event))
end
def event_url(event)
url_helpers.topic_url(*url_helper_parameter(event))
url_helpers.project_forum_topic_url(*url_helper_parameter(event))
end
private
@@ -83,9 +83,19 @@ class Activities::MessageActivityProvider < Activities::BaseActivityProvider
is_reply = event["parent_id"].present?
if is_reply
{ id: event["parent_id"], r: event["journable_id"], anchor: "message-#{event['journable_id']}" }
{
project_id: event["project_id"],
forum: event["forum_id"],
id: event["parent_id"],
r: event["journable_id"],
anchor: "message-#{event['journable_id']}"
}
else
[event["journable_id"]]
{
project_id: event["project_id"],
forum_id: event["forum_id"],
id: event["journable_id"]
}
end
end
end
@@ -50,16 +50,16 @@ class Activities::NewsActivityProvider < Activities::BaseActivityProvider
end
def event_path(event)
url_helpers.news_path(url_helper_parameter(event))
url_helpers.project_news_path(url_helper_parameter(event))
end
def event_url(event)
url_helpers.news_url(url_helper_parameter(event))
url_helpers.project_news_url(url_helper_parameter(event))
end
private
def url_helper_parameter(event)
event["journable_id"]
{ project_id: event["project_id"], id: event["journable_id"] }
end
end
+2 -2
View File
@@ -258,7 +258,7 @@ class CustomField < ApplicationRecord
name =~ /\A(.+)CustomField\z/
begin
$1.constantize
rescue StandardError
rescue NameError
nil
end
end
@@ -369,7 +369,7 @@ class CustomField < ApplicationRecord
# Use a ruby finder to avoid hitting the database with N+1 queries on the project list page,
# the errors are eager loaded via the Queries::Projects::CustomFieldContext.
calculated_value_errors.find { it.customized_id == customized.id }
calculated_value_errors.find { it.customized == customized }
end
private
@@ -39,7 +39,7 @@ module Exports
# Takes a WorkPackage or Project and an attribute and returns the value to be exported.
def retrieve_value(object)
custom_field = find_custom_field(object)
return "" if custom_field.nil?
return nil if custom_field.nil?
format_for_export(object, custom_field)
end
@@ -68,8 +68,8 @@ module Exports
##
# Finds a custom field from the attribute identifier
def find_custom_field(object)
id = attribute.to_s.sub("cf_", "").to_i
object.available_custom_fields.detect { |cf| cf.id == id }
id = attribute.to_s.delete_prefix("cf_").to_i
object.available_custom_fields.find { it.id == id }
end
end
end
@@ -101,7 +101,7 @@ class Journable::HistoricActiveRecordRelation < ActiveRecord::Relation
#
# SELECT * from work_packages
def build_arel(connection, aliases = nil)
def build_arel(aliases = nil)
substitute_join_tables_in_where_clause(self)
# Based on the previous modifications, build the algebra object and prepend
+1 -1
View File
@@ -34,7 +34,7 @@ module Members::Scopes
class_methods do
# Find all members visible to the inquiring user
def visible(user)
def visible(user = User.current)
if user.admin?
visible_for_admins
else
+3 -3
View File
@@ -174,9 +174,9 @@ class Project < ApplicationRecord
register_journal_formatted_fields "status_code", formatter_key: :project_status_code
register_journal_formatted_fields "public", formatter_key: :visibility
register_journal_formatted_fields "parent_id", formatter_key: :subproject_named_association
register_journal_formatted_fields /custom_fields_\d+/, formatter_key: :custom_field
register_journal_formatted_fields /^project_phase_\d+_active$/, formatter_key: :project_phase_active
register_journal_formatted_fields /^project_phase_\d+_date_range$/, formatter_key: :project_phase_dates
register_journal_formatted_fields /\Acustom_fields_\d+\z/, formatter_key: :custom_field
register_journal_formatted_fields /\Aproject_phase_\d+_active\z/, formatter_key: :project_phase_active
register_journal_formatted_fields /\Aproject_phase_\d+_date_range\z/, formatter_key: :project_phase_dates
has_paper_trail
@@ -188,12 +188,12 @@ class Project::PDFExport::ProjectInitiation < Exports::Exporter
.where(id: enabled_in_wizard_ids)
.group_by(&:project_custom_field_section)
.map do |section, custom_fields|
{
caption: section.name,
fields: custom_fields.map do |custom_field|
{ key: "cf_#{custom_field.id}", caption: custom_field.name, custom_field: }
end
}
{
caption: section.name,
fields: custom_fields.map do |custom_field|
{ key: "cf_#{custom_field.id}", caption: custom_field.name, custom_field: }
end
}
end
end
@@ -284,7 +284,7 @@ class Project::PDFExport::ProjectInitiation < Exports::Exporter
def project_initiation_work_package_status
return nil if project.project_creation_wizard_artifact_work_package_id.blank?
work_package = WorkPackage.find_by(id: project.project_creation_wizard_artifact_work_package_id)
work_package = WorkPackage.visible.find_by(id: project.project_creation_wizard_artifact_work_package_id)
work_package&.status
end
@@ -82,6 +82,6 @@ class Queries::Principals::Filters::InternalMentionableOnWorkPackageFilter <
end
def work_package
WorkPackage.find(values.first)
WorkPackage.visible.find(values.first)
end
end
@@ -49,7 +49,7 @@ class Queries::WorkPackages::Filter::RelatableFilter < Queries::WorkPackages::Fi
end
def apply_to(query_scope)
query_scope.relatable(WorkPackage.find_by(id: values.first), scope_operator)
query_scope.relatable(WorkPackage.visible.find_by(id: values.first), scope_operator)
end
private
+1 -89
View File
@@ -31,6 +31,7 @@
class Setting < ApplicationRecord
class NotWritableError < StandardError; end
extend Accessors
extend Aliases
extend MailSettings
@@ -74,73 +75,6 @@ class Setting < ApplicationRecord
Big5-HKSCS
TIS-620).freeze
class << self
def create_setting(name, value = {})
::Settings::Definition.add(name, **value.symbolize_keys)
end
def create_setting_accessors(name)
return if [:installation_uuid].include?(name.to_sym)
# Defines getter and setter for each setting
# Then setting values can be read using: Setting.some_setting_name
# or set using Setting.some_setting_name = "some value"
src = <<-END_SRC
def self.#{name}
# when running too early, there is no settings table. do nothing
self[:#{name}] if settings_table_exists_yet?
end
def self.#{name}?
# when running too early, there is no settings table. do nothing
return unless settings_table_exists_yet?
definition = Settings::Definition[:#{name}]
if definition.format != :boolean
ActiveSupport::Deprecation.new.warn "Calling #{self}.#{name}? is deprecated since it is not a boolean", caller_locations
end
value = self[:#{name}]
ActiveRecord::Type::Boolean.new.cast(value) || false
end
def self.#{name}=(value)
if settings_table_exists_yet?
self[:#{name}] = value
else
logger.warn "Trying to save a setting named '#{name}' while there is no 'setting' table yet. This setting will not be saved!"
nil # when running too early, there is no settings table. do nothing
end
end
def self.#{name}_writable?
Settings::Definition[:#{name}].writable?
end
END_SRC
class_eval src, __FILE__, __LINE__
end
def method_missing(method, *, &)
if exists?(accessor_base_name(method))
create_setting_accessors(accessor_base_name(method))
send(method, *)
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
exists?(accessor_base_name(method_name)) || super
end
private
def accessor_base_name(name)
name.to_s.sub(/(_writable\?)|(\?)|=\z/, "")
end
end
validates :name,
uniqueness: true,
inclusion: {
@@ -223,28 +157,6 @@ class Setting < ApplicationRecord
Settings::Definition[name].present?
end
def self.installation_uuid
if settings_table_exists_yet?
# we avoid the default getters and setters since the cache messes things up
setting = find_or_initialize_by(name: "installation_uuid")
if setting.value.blank?
setting.value = generate_installation_uuid
setting.save!
end
setting.value
else
"unknown"
end
end
def self.generate_installation_uuid
if Rails.env.test?
"test"
else
SecureRandom.uuid
end
end
%i[emails_header emails_footer].each do |mail|
src = <<-END_SRC
def self.localized_#{mail}
+109
View File
@@ -0,0 +1,109 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class Setting
# Dynamically defines getter, setter, boolean, and writable? class methods
# for each setting. Methods are lazily created via method_missing when a
# setting is first accessed.
#
# After creation, setting values can be read using: Setting.some_setting_name
# or set using: Setting.some_setting_name = "some value"
module Accessors
def create_setting(name, value = {})
::Settings::Definition.add(name, **value.symbolize_keys)
end
def create_setting_accessors(name)
define_setting_getter(name)
define_setting_boolean_getter(name)
define_setting_setter(name)
define_setting_writable_check(name)
end
def method_missing(method, *, &)
if exists?(accessor_base_name(method))
create_setting_accessors(accessor_base_name(method))
send(method, *)
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
exists?(accessor_base_name(method_name)) || super
end
private
def define_setting_getter(name)
define_singleton_method(name) do
# when running too early, there is no settings table. do nothing
self[name] if settings_table_exists_yet?
end
end
def define_setting_boolean_getter(name)
define_singleton_method(:"#{name}?") do
definition = Settings::Definition[name]
if definition.format != :boolean
ActiveSupport::Deprecation.new.warn "Calling #{self}.#{name}? is deprecated since it is not a boolean", caller_locations
end
# Use accessor to go through same table check
value = public_send(name)
ActiveRecord::Type::Boolean.new.cast(value) || false
end
end
def define_setting_setter(name)
define_singleton_method(:"#{name}=") do |value|
if settings_table_exists_yet?
self[name] = value
else
logger.warn "Trying to save a setting named '#{name}' while there is no 'setting' table yet. " \
"This setting will not be saved!"
nil # when running too early, there is no settings table. do nothing
end
end
end
def define_setting_writable_check(name)
define_singleton_method(:"#{name}_writable?") do
Settings::Definition[name].writable?
end
end
def accessor_base_name(name)
name.to_s.sub(/(_writable\?)|(\?)|=\z/, "")
end
end
end
+1
View File
@@ -30,5 +30,6 @@
module Token
class API < Named
prefix :opapi
end
end
+2
View File
@@ -32,6 +32,8 @@ module Token
class AutoLogin < HashedToken
include ExpirableToken
prefix :opal
has_many :autologin_session_links,
class_name: "Sessions::AutologinSessionLink",
foreign_key: "token_id",
+2
View File
@@ -30,6 +30,8 @@
module Token
class Backup < HashedToken
prefix :opbk
def ready?
return false if created_at.nil?
+29 -14
View File
@@ -64,22 +64,37 @@ module Token
# Delete previous token of this type upon save
before_save :delete_previous_token
##
# Find a token from the token value
def self.find_by_plaintext_value(input)
find_by(value: input)
end
class << self
##
# A DSL method allowing to define a prefix for all generated tokens, making it possible to recognize
# the purpose of a token by looking at the token value.
#
# class MyToken < HashedToken
# prefix :my
# end
def prefix(value = nil)
@prefix = value.to_s if value
##
# Find tokens for the given user
def self.for_user(user)
where(user:)
end
@prefix
end
##
# Generate a random hex token value
def self.generate_token_value
SecureRandom.hex(32)
##
# Find a token from the token value
def find_by_plaintext_value(input)
find_by(value: input)
end
##
# Find tokens for the given user
def for_user(user)
where(user:)
end
##
# Generate a random hex token value
def generate_token_value
[prefix, SecureRandom.hex(32)].compact.join("-")
end
end
##
+1 -1
View File
@@ -53,7 +53,7 @@ module Token
class << self
def create_and_return_value(user)
create(user:).plain_value
create!(user:).plain_value
end
##
+2
View File
@@ -30,6 +30,8 @@
module Token
class ICal < HashedToken
prefix :opical
# restrict the usage of one ical token to one query (calendar)
has_one :ical_token_query_assignment, required: true, dependent: :destroy, foreign_key: :ical_token_id,
class_name: "ICalTokenQueryAssignment", inverse_of: :ical_token
+1 -1
View File
@@ -448,7 +448,7 @@ class User < Principal
end
def self.find_by_api_key(key)
return nil unless Setting.rest_api_enabled?
return nil unless Setting.api_tokens_enabled?
token = Token::API.find_by_plaintext_value(key)
+4 -3
View File
@@ -96,11 +96,11 @@ module WorkPackage::Journalized
register_journal_formatted_fields "done_ratio", "derived_done_ratio", formatter_key: :percentage
register_journal_formatted_fields "description", formatter_key: :diff
register_journal_formatted_fields "schedule_manually", formatter_key: :schedule_manually
register_journal_formatted_fields /attachments_?\d+/, formatter_key: :attachment
register_journal_formatted_fields /custom_fields_\d+/, formatter_key: :custom_field
register_journal_formatted_fields /\Aattachments_?\d+\z/, formatter_key: :attachment
register_journal_formatted_fields /\Acustom_fields_\d+\z/, formatter_key: :custom_field
register_journal_formatted_fields "ignore_non_working_days", formatter_key: :ignore_non_working_days
register_journal_formatted_fields "cause", formatter_key: :cause
register_journal_formatted_fields /file_links_?\d+/, formatter_key: :file_link
register_journal_formatted_fields /\Afile_links_?\d+\z/, formatter_key: :file_link
register_journal_formatted_fields "project_phase_definition_id", formatter_key: :project_phase_definition
# Joined
@@ -110,6 +110,7 @@ module WorkPackage::Journalized
:assigned_to_id, :priority_id,
:category_id, :version_id,
:author_id, :responsible_id,
:sprint_id,
formatter_key: :named_association
register_journal_formatted_fields :start_date, :due_date, formatter_key: :datetime
register_journal_formatted_fields :subject, formatter_key: :plaintext
+1 -3
View File
@@ -32,9 +32,7 @@ module WorkPackage::Validations
extend ActiveSupport::Concern
included do
validates :subject, :priority, :project, :type, :author, :status, presence: true
validates :subject, length: { maximum: 255 }
validates :priority, :project, :type, :author, :status, presence: true
validates :done_ratio, inclusion: { in: 0..100 }, numericality: true, allow_nil: true
validates :estimated_hours, numericality: { allow_nil: true, greater_than_or_equal_to: 0 }
validates :remaining_hours, numericality: { allow_nil: true, greater_than_or_equal_to: 0 }
+3
View File
@@ -52,6 +52,9 @@ module BasicData
model_class
.create!(model_attributes(model_data))
.tap { |model| seed_data.store_reference(model_data["reference"], model) }
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error { "Failed to create #{model_class} seed_data: %e" }
raise e
end
def mapped_models_data
@@ -58,6 +58,7 @@ class Journals::CreateService
FROM custom_values
WHERE
#{only_if_created_sql}
AND #{availability_condition}
AND custom_values.customized_id = :journable_id
AND custom_values.customized_type = :journable_class_name
AND custom_values.value IS NOT NULL
@@ -72,16 +73,17 @@ class Journals::CreateService
FROM
(
SELECT
custom_field_id,
ARRAY_AGG(#{normalize_newlines_sql('custom_values.value')} ORDER BY value) AS value
custom_values.custom_field_id,
ARRAY_AGG(#{normalize_newlines_sql('custom_values.value')} ORDER BY value) AS value
FROM
custom_values
custom_values
WHERE
custom_values.customized_id = :journable_id
#{availability_condition}
AND custom_values.customized_id = :journable_id
AND custom_values.customized_type = :customized_type
AND custom_values.value != ''
GROUP BY
custom_field_id
custom_values.custom_field_id
) current_values
FULL JOIN
(
@@ -100,5 +102,23 @@ class Journals::CreateService
current_values.value IS DISTINCT FROM journal_values.value
SQL
end
private
def availability_condition
return "1 = 1" unless journable.is_a?(Project)
<<~SQL # rubocop:disable Rails/SquishedSQLHeredocs
EXISTS (
SELECT 1
FROM custom_fields
LEFT JOIN project_custom_field_project_mappings
ON project_custom_field_project_mappings.custom_field_id = custom_fields.id
AND project_custom_field_project_mappings.project_id = :journable_id
WHERE custom_fields.id = custom_values.custom_field_id
AND (custom_fields.is_for_all = TRUE OR project_custom_field_project_mappings.project_id IS NOT NULL)
)
SQL
end
end
end
+1 -2
View File
@@ -37,9 +37,8 @@ module McpResources
default_description "Access work packages of this OpenProject instance."
def read(id:)
work_package = ::WorkPackage.find_by(id:)
work_package = ::WorkPackage.visible.find_by(id:)
return nil if work_package.nil?
return nil unless current_user.allowed_in_work_package?(:view_work_packages, work_package)
API::V3::WorkPackages::WorkPackageRepresenter.create(work_package, current_user:, embed_links: true)
end
@@ -182,9 +182,8 @@ module Projects::CreationWizard
end
def assignee_mention_tag
return if assigned_to_id.nil?
principal = Principal.find(assigned_to_id)
principal = Principal.visible.find_by(id: assigned_to_id)
return "" if principal.nil?
ApplicationController.helpers.content_tag(
"mention",

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