mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Merge branch 'dev' into merge-release/17.1-20260213121048
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -72,6 +72,7 @@ jobs:
|
||||
samachon,
|
||||
shiroginne,
|
||||
toy,
|
||||
tiroessler,
|
||||
ulferts,
|
||||
vonTronje,
|
||||
vspielau,
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
+89
@@ -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
|
||||
+16
@@ -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
|
||||
+50
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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
@@ -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}
|
||||
|
||||
@@ -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
|
||||
@@ -30,5 +30,6 @@
|
||||
|
||||
module Token
|
||||
class API < Named
|
||||
prefix :opapi
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
module Token
|
||||
class Backup < HashedToken
|
||||
prefix :opbk
|
||||
|
||||
def ready?
|
||||
return false if created_at.nil?
|
||||
|
||||
|
||||
+29
-14
@@ -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
|
||||
|
||||
##
|
||||
|
||||
@@ -53,7 +53,7 @@ module Token
|
||||
|
||||
class << self
|
||||
def create_and_return_value(user)
|
||||
create(user:).plain_value
|
||||
create!(user:).plain_value
|
||||
end
|
||||
|
||||
##
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user