+ <%= (local_assigns[:error_response] ? error_response : @pre_auth.error_response).body[:error_description] %> ++
diff --git a/.github/workflows/brakeman.yml b/.github/workflows/brakeman.yml
index ca55cdbbe3..c6c0935a77 100644
--- a/.github/workflows/brakeman.yml
+++ b/.github/workflows/brakeman.yml
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
# Will run Brakeman checks on dependencies
# https://github.com/marketplace/actions/brakeman-action
diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml
index c7d111d97e..29a265c861 100644
--- a/.github/workflows/danger.yml
+++ b/.github/workflows/danger.yml
@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
# Install Ruby and run bundler
- uses: ruby/setup-ruby@v1
diff --git a/.github/workflows/docker-push-image.yml b/.github/workflows/docker-push-image.yml
index 96c99fcbc4..06cd8dae9b 100644
--- a/.github/workflows/docker-push-image.yml
+++ b/.github/workflows/docker-push-image.yml
@@ -19,14 +19,14 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Check out the repo
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
# Buildx allows for advanced Docker build features like multi-platform builds and layer caching
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@v4
- name: Log in to Docker Hub
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
+ uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -34,7 +34,7 @@ jobs:
# Extract metadata for app image
- name: Extract metadata (tags) for Docker app Image
id: meta_app
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@v6
with:
images: ualbertalib/dmp_roadmap
flavor: |
@@ -55,7 +55,7 @@ jobs:
# Extract metadata for assets image
- name: Extract metadata (tags) for Docker assets image
id: meta_assets
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@v6
with:
images: ualbertalib/dmp_roadmap
flavor: |
@@ -75,7 +75,7 @@ jobs:
- name: Build and push the app stage image
- uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
+ uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294
with:
context: .
file: Dockerfile.production
@@ -89,7 +89,7 @@ jobs:
cache-to: type=registry,ref=ualbertalib/dmp_roadmap:build-cache-app,mode=max
- name: Build and push the assets stage image
- uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
+ uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294
with:
context: .
file: Dockerfile.production
diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml
index 58f7c86714..51a04f0dc7 100644
--- a/.github/workflows/eslint.yml
+++ b/.github/workflows/eslint.yml
@@ -8,8 +8,8 @@ jobs:
runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v5
- - uses: actions/setup-node@v5
+ - uses: actions/checkout@v6
+ - uses: actions/setup-node@v6
with:
node-version: '20.15.1'
cache: 'yarn'
diff --git a/.github/workflows/mysql.yml b/.github/workflows/mysql.yml
index df69278319..4add0de392 100644
--- a/.github/workflows/mysql.yml
+++ b/.github/workflows/mysql.yml
@@ -14,7 +14,7 @@ jobs:
steps:
# Checkout the repo
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
# Install Ruby and run bundler
- uses: ruby/setup-ruby@v1
@@ -23,7 +23,7 @@ jobs:
bundler-cache: true
# Install Node
- - uses: actions/setup-node@v5
+ - uses: actions/setup-node@v6
with:
node-version: '20.15.1'
cache: 'yarn'
diff --git a/.github/workflows/postgres.yml b/.github/workflows/postgres.yml
index cfed518d35..edad58ba01 100644
--- a/.github/workflows/postgres.yml
+++ b/.github/workflows/postgres.yml
@@ -30,7 +30,7 @@ jobs:
steps:
# Checkout the repo
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
# Install Ruby and run bundler
- uses: ruby/setup-ruby@v1
@@ -42,7 +42,7 @@ jobs:
## /home/runner/runners/2.301.1/externals/node12/bin/node: --openssl-legacy-provider is not allowed in NODE_OPTIONS
# Install Node
- - uses: actions/setup-node@v5
+ - uses: actions/setup-node@v6
with:
node-version: '20.15.1'
cache: 'yarn'
@@ -82,7 +82,7 @@ jobs:
- name: Remove image-bundled Chrome
run: sudo apt-get purge google-chrome-stable
- name: Setup stable Chrome
- uses: browser-actions/setup-chrome@v1
+ uses: browser-actions/setup-chrome@v2
with:
chrome-version: 128
install-chromedriver: true
diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml
index 9a708d4c9f..f893ec927d 100644
--- a/.github/workflows/rubocop.yml
+++ b/.github/workflows/rubocop.yml
@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: 'Determine Ruby and Bundler Versions from Gemfile.lock'
run: |
diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml
index 5975b8d3bb..1cb13e5aed 100644
--- a/.github/workflows/ruby.yml
+++ b/.github/workflows/ruby.yml
@@ -40,7 +40,7 @@ jobs:
steps:
# Checkout the repo
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
# Install Ruby and run bundler
- uses: ruby/setup-ruby@v1
@@ -49,7 +49,7 @@ jobs:
bundler-cache: true
# Install Node
- - uses: actions/setup-node@v5
+ - uses: actions/setup-node@v6
with:
node-version: '20.15.1'
cache: 'yarn'
@@ -86,7 +86,7 @@ jobs:
- name: Remove image-bundled Chrome
run: sudo apt-get purge google-chrome-stable
- name: Setup stable Chrome
- uses: browser-actions/setup-chrome@v1
+ uses: browser-actions/setup-chrome@v2
with:
chrome-version: 128
install-chromedriver: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6a0af4322..e6d5feb408 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,72 @@
# Changelog
+## [Unreleased]
+
+### Added
+
+ - Add rake task to clean up unmanaged orgs w/ users [#1250](https://github.com/portagenetwork/roadmap/pull/1250)
+
+ - Implement tiered Rack::Attack throttles [#1254](https://github.com/portagenetwork/roadmap/pull/1254)
+
+ - Add Internal v2 API Access Token Generation for Users [#1279](https://github.com/portagenetwork/roadmap/pull/1279)
+
+ - Add `bundle exec rails db:migrate` to entrypoint.sh [#1278](https://github.com/portagenetwork/roadmap/pull/1278)
+
+ - Add copy button next to V2 API Token [#1283](https://github.com/portagenetwork/roadmap/pull/1283)
+
+ - Initial v2 API Implementation & Doorkeeper OAuth Integration [#1276](https://github.com/portagenetwork/roadmap/pull/1276)
+
+ - API v2: Sanitize user-supplied fields in responses [#1303](https://github.com/portagenetwork/roadmap/pull/1303)
+
+ - Add v2 API documentation to API Access page [#1300](https://github.com/portagenetwork/roadmap/pull/1300)
+
+### Changed
+
+ - Upgrade ROR API From V1 to V2 [#1247](https://github.com/portagenetwork/roadmap/pull/1247)
+
+ - Update plan json export to use V2 API complete plan endpoint [#1293](https://github.com/portagenetwork/roadmap/pull/1293)
+
+ - Redesign and Edit Welcome, Help, and About pages and Remove Public DMPs Page [#1299](https://github.com/portagenetwork/roadmap/pull/1299)
+
+ - Update expiry time of v2 API internal access token [#1308](https://github.com/portagenetwork/roadmap/pull/1308)
+
+### Dependency Updates
+
+ - chore(deps): bump nginx from 1.29.3-alpine to 1.29.4-alpine [#1246](https://github.com/portagenetwork/roadmap/pull/1246)
+
+ - chore(deps): bump actions/checkout from 5 to 6 [#1236](https://github.com/portagenetwork/roadmap/pull/1236)
+
+ - chore(deps): bump omniauth from 2.1.2 to 2.1.4 [#1201](https://github.com/portagenetwork/roadmap/pull/1201)
+
+ - chore(deps): bump browser-actions/setup-chrome from 1 to 2 [#1138](https://github.com/portagenetwork/roadmap/pull/1138)
+
+ - chore(deps): bump webpack from 5.94.0 to 5.102.1 [#1206](https://github.com/portagenetwork/roadmap/pull/1206)
+
+ - chore(deps): bump actions/setup-node from 5 to 6 [#1211](https://github.com/portagenetwork/roadmap/pull/1211)
+
+ - chore(deps): bump surnet/alpine-wkhtmltopdf from 3.22.0-0.12.6-small to 3.23.2-0.12.6-small [#1258](https://github.com/portagenetwork/roadmap/pull/1258)
+
+ - chore(deps): bump docker/login-action from 3.6.0 to 3.7.0 [#1268](https://github.com/portagenetwork/roadmap/pull/1268)
+
+ - chore(deps): bump docker/build-push-action from 6.18.0 to 6.19.2 [#1277](https://github.com/portagenetwork/roadmap/pull/1277)
+
+ - chore(deps): bump nginx from 1.29.4-alpine to 1.29.5-alpine [#1271](https://github.com/portagenetwork/roadmap/pull/1271)
+
+ - `bundle update httparty` [#1291](https://github.com/portagenetwork/roadmap/pull/1291)
+ - (See [#1288](https://github.com/portagenetwork/roadmap/pull/1288) for more)
+
+ - chore(deps): bump docker/login-action from 3.7.0 to 4.0.0 [#1290](https://github.com/portagenetwork/roadmap/pull/1290)
+
+ - chore(deps): bump docker/setup-buildx-action from 3 to 4 [#1295](https://github.com/portagenetwork/roadmap/pull/1295)
+
+ - chore(deps): bump docker/metadata-action from 5 to 6 [#1298](https://github.com/portagenetwork/roadmap/pull/1298)
+
+ - chore(deps): bump docker/build-push-action from 6.19.2 to 7.0.0 [#1297](https://github.com/portagenetwork/roadmap/pull/1297)
+
+ - chore(deps): bump nginx from 1.29.5-alpine to 1.29.6-alpine [#1302](https://github.com/portagenetwork/roadmap/pull/1302)
+
+ - chore(deps): bump eslint-plugin-import from 2.27.5 to 2.32.0 [#1149](https://github.com/portagenetwork/roadmap/pull/1149)
+
## [4.1.1+portage-4.6.2]
### Added
diff --git a/Dockerfile.production b/Dockerfile.production
index 628d6ba86f..2721217154 100644
--- a/Dockerfile.production
+++ b/Dockerfile.production
@@ -1,4 +1,4 @@
-FROM surnet/alpine-wkhtmltopdf:3.22.0-0.12.6-small AS wkhtmltopdf
+FROM surnet/alpine-wkhtmltopdf:3.23.2-0.12.6-small AS wkhtmltopdf
FROM ruby:3.1.4-alpine AS builder
@@ -139,7 +139,7 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
ENTRYPOINT ["/usr/bin/entrypoint.sh"]
# nginx stage to serve static assets
-FROM nginx:1.29.3-alpine AS assets
+FROM nginx:1.29.6-alpine AS assets
ENV INSTALL_PATH=/usr/src/app
diff --git a/Gemfile b/Gemfile
index fcc7af1783..9414e12657 100644
--- a/Gemfile
+++ b/Gemfile
@@ -88,6 +88,8 @@ gem 'devise'
# An invitation strategy for Devise (https://github.com/scambra/devise_invitable)
gem 'devise_invitable'
+gem 'doorkeeper'
+
# A generalized Rack framework for multiple-provider authentication.
# (https://github.com/omniauth/omniauth)
gem 'omniauth'
diff --git a/Gemfile.lock b/Gemfile.lock
index 9b6cf44b43..1ed7eea62e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,722 +1,726 @@
-GIT
- remote: https://github.com/ualbertalib/translation_io_rails
- revision: f60a5427372b51348eb218755e275f0f34d19746
- specs:
- translation (1.22)
- gettext (~> 3.2, >= 3.2.5)
-
-GEM
- remote: https://rubygems.org/
- specs:
- actioncable (6.1.7.9)
- actionpack (= 6.1.7.9)
- activesupport (= 6.1.7.9)
- nio4r (~> 2.0)
- websocket-driver (>= 0.6.1)
- actionmailbox (6.1.7.9)
- actionpack (= 6.1.7.9)
- activejob (= 6.1.7.9)
- activerecord (= 6.1.7.9)
- activestorage (= 6.1.7.9)
- activesupport (= 6.1.7.9)
- mail (>= 2.7.1)
- actionmailer (6.1.7.9)
- actionpack (= 6.1.7.9)
- actionview (= 6.1.7.9)
- activejob (= 6.1.7.9)
- activesupport (= 6.1.7.9)
- mail (~> 2.5, >= 2.5.4)
- rails-dom-testing (~> 2.0)
- actionpack (6.1.7.9)
- actionview (= 6.1.7.9)
- activesupport (= 6.1.7.9)
- rack (~> 2.0, >= 2.0.9)
- rack-test (>= 0.6.3)
- rails-dom-testing (~> 2.0)
- rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (6.1.7.9)
- actionpack (= 6.1.7.9)
- activerecord (= 6.1.7.9)
- activestorage (= 6.1.7.9)
- activesupport (= 6.1.7.9)
- nokogiri (>= 1.8.5)
- actionview (6.1.7.9)
- activesupport (= 6.1.7.9)
- builder (~> 3.1)
- erubi (~> 1.4)
- rails-dom-testing (~> 2.0)
- rails-html-sanitizer (~> 1.1, >= 1.2.0)
- activejob (6.1.7.9)
- activesupport (= 6.1.7.9)
- globalid (>= 0.3.6)
- activemodel (6.1.7.9)
- activesupport (= 6.1.7.9)
- activerecord (6.1.7.9)
- activemodel (= 6.1.7.9)
- activesupport (= 6.1.7.9)
- activerecord-nulldb-adapter (1.1.1)
- activerecord (>= 6.0, < 8.1)
- activerecord_json_validator (2.1.5)
- activerecord (>= 4.2.0, < 8)
- json_schemer (~> 0.2.18)
- activestorage (6.1.7.9)
- actionpack (= 6.1.7.9)
- activejob (= 6.1.7.9)
- activerecord (= 6.1.7.9)
- activesupport (= 6.1.7.9)
- marcel (~> 1.0)
- mini_mime (>= 1.1.0)
- activesupport (6.1.7.9)
- concurrent-ruby (~> 1.0, >= 1.0.2)
- i18n (>= 1.6, < 2)
- minitest (>= 5.1)
- tzinfo (~> 2.0)
- zeitwerk (~> 2.3)
- addressable (2.8.7)
- public_suffix (>= 2.0.2, < 7.0)
- aes_key_wrap (1.1.0)
- annotate (3.2.0)
- activerecord (>= 3.2, < 8.0)
- rake (>= 10.4, < 14.0)
- annotate_gem (0.0.14)
- bundler (>= 1.1)
- api-pagination (5.0.0)
- ast (2.4.2)
- attr_required (1.0.2)
- autoprefixer-rails (10.4.19.0)
- execjs (~> 2)
- base64 (0.1.2)
- bcrypt (3.1.20)
- better_errors (2.10.1)
- erubi (>= 1.0.0)
- rack (>= 0.9.0)
- rouge (>= 1.0.0)
- bigdecimal (3.3.1)
- bindata (2.5.0)
- bindex (0.8.1)
- binding_of_caller (1.0.1)
- debug_inspector (>= 1.2.0)
- bootsnap (1.18.3)
- msgpack (~> 1.2)
- brakeman (7.0.0)
- racc
- builder (3.3.0)
- bullet (7.1.6)
- activesupport (>= 3.0.0)
- uniform_notifier (~> 1.11)
- bundle-audit (0.1.0)
- bundler-audit
- bundler-audit (0.9.1)
- bundler (>= 1.2.0, < 3)
- thor (~> 1.0)
- byebug (11.1.3)
- capybara (3.39.2)
- addressable
- matrix
- mini_mime (>= 0.1.3)
- nokogiri (~> 1.8)
- rack (>= 1.6.0)
- rack-test (>= 0.6.3)
- regexp_parser (>= 1.5, < 3.0)
- xpath (~> 3.2)
- claide (1.1.0)
- claide-plugins (0.9.2)
- cork
- nap
- open4 (~> 1.3)
- coderay (1.1.3)
- colored2 (3.1.2)
- concurrent-ruby (1.3.4)
- contact_us (1.2.0)
- rails (>= 4.2.0)
- cork (0.3.0)
- colored2 (~> 3.1)
- crack (1.0.0)
- bigdecimal
- rexml
- crass (1.0.6)
- cssbundling-rails (1.4.1)
- railties (>= 6.0.0)
- csv (3.3.5)
- danger (9.4.3)
- claide (~> 1.0)
- claide-plugins (>= 0.9.2)
- colored2 (~> 3.1)
- cork (~> 0.1)
- faraday (>= 0.9.0, < 3.0)
- faraday-http-cache (~> 2.0)
- git (~> 1.13)
- kramdown (~> 2.3)
- kramdown-parser-gfm (~> 1.0)
- no_proxy_fix
- octokit (>= 4.0)
- terminal-table (>= 1, < 4)
- database_cleaner (2.0.2)
- database_cleaner-active_record (>= 2, < 3)
- database_cleaner-active_record (2.1.0)
- activerecord (>= 5.a)
- database_cleaner-core (~> 2.0.0)
- database_cleaner-core (2.0.1)
- date (3.4.1)
- debug_inspector (1.2.0)
- devise (4.9.4)
- bcrypt (~> 3.0)
- orm_adapter (~> 0.1)
- railties (>= 4.1.0)
- responders
- warden (~> 1.2.3)
- devise_invitable (2.0.9)
- actionmailer (>= 5.0)
- devise (>= 4.6)
- diff-lcs (1.5.1)
- dotenv (2.8.1)
- dotenv-rails (2.8.1)
- dotenv (= 2.8.1)
- railties (>= 3.2)
- dragonfly (1.4.1)
- addressable (~> 2.3)
- multi_json (~> 1.0)
- ostruct (~> 0.6.1)
- rack (>= 1.3)
- dragonfly-s3_data_store (1.3.0)
- dragonfly (~> 1.0)
- fog-aws
- ecma-re-validator (0.4.0)
- regexp_parser (~> 2.2)
- email_validator (2.2.4)
- activemodel
- erubi (1.13.1)
- excon (0.104.0)
- execjs (2.10.0)
- factory_bot (6.2.1)
- activesupport (>= 5.0.0)
- factory_bot_rails (6.2.0)
- factory_bot (~> 6.2.0)
- railties (>= 5.0.0)
- faker (3.4.1)
- i18n (>= 1.8.11, < 2)
- faraday (2.9.0)
- faraday-net_http (>= 2.0, < 3.2)
- faraday-follow_redirects (0.3.0)
- faraday (>= 1, < 3)
- faraday-http-cache (2.5.1)
- faraday (>= 0.8)
- faraday-net_http (3.1.0)
- net-http
- ffi (1.17.2-arm64-darwin)
- ffi (1.17.2-x86_64-linux-gnu)
- ffi (1.17.2-x86_64-linux-musl)
- flag_shih_tzu (0.3.23)
- fog-aws (3.21.0)
- fog-core (~> 2.1)
- fog-json (~> 1.1)
- fog-xml (~> 0.1)
- fog-core (2.3.0)
- builder
- excon (~> 0.71)
- formatador (>= 0.2, < 2.0)
- mime-types
- fog-json (1.2.0)
- fog-core
- multi_json (~> 1.10)
- fog-xml (0.1.4)
- fog-core
- nokogiri (>= 1.5.11, < 2.0.0)
- formatador (1.1.0)
- forwardable (1.3.3)
- fuubar (2.5.1)
- rspec-core (~> 3.0)
- ruby-progressbar (~> 1.4)
- gettext (3.5.1)
- erubi
- locale (>= 2.0.5)
- prime
- racc
- text (>= 1.3.0)
- git (1.19.1)
- addressable (~> 2.8)
- rchardet (~> 1.8)
- globalid (1.2.1)
- activesupport (>= 6.1)
- guard (2.19.1)
- formatador (>= 0.2.4)
- listen (>= 2.7, < 4.0)
- logger (~> 1.6)
- lumberjack (>= 1.0.12, < 2.0)
- nenv (~> 0.1)
- notiffany (~> 0.0)
- ostruct (~> 0.6)
- pry (>= 0.13.0)
- shellany (~> 0.0)
- thor (>= 0.18.1)
- hana (1.3.7)
- hashdiff (1.1.0)
- hashie (5.0.0)
- highline (3.1.2)
- reline
- htmltoword (1.1.1)
- actionpack
- nokogiri
- rubyzip (>= 1.0)
- httparty (0.24.0)
- csv
- mini_mime (>= 1.0.0)
- multi_xml (>= 0.5.2)
- i18n (1.14.6)
- concurrent-ruby (~> 1.0)
- io-console (0.8.0)
- jbuilder (2.12.0)
- actionview (>= 5.0.0)
- activesupport (>= 5.0.0)
- jsbundling-rails (1.3.0)
- railties (>= 6.0.0)
- json (2.7.2)
- json-jwt (1.16.6)
- activesupport (>= 4.2)
- aes_key_wrap
- base64
- bindata
- faraday (~> 2.0)
- faraday-follow_redirects
- json_schemer (0.2.25)
- ecma-re-validator (~> 0.3)
- hana (~> 1.3)
- regexp_parser (~> 2.0)
- simpleidn (~> 0.2)
- uri_template (~> 0.7)
- jwt (2.10.1)
- base64
- kaminari (1.2.2)
- activesupport (>= 4.1.0)
- kaminari-actionview (= 1.2.2)
- kaminari-activerecord (= 1.2.2)
- kaminari-core (= 1.2.2)
- kaminari-actionview (1.2.2)
- actionview
- kaminari-core (= 1.2.2)
- kaminari-activerecord (1.2.2)
- activerecord
- kaminari-core (= 1.2.2)
- kaminari-core (1.2.2)
- kramdown (2.4.0)
- rexml
- kramdown-parser-gfm (1.1.0)
- kramdown (~> 2.0)
- language_server-protocol (3.17.0.3)
- ledermann-rails-settings (2.6.2)
- activerecord (>= 6.1)
- listen (3.9.0)
- rb-fsevent (~> 0.10, >= 0.10.3)
- rb-inotify (~> 0.9, >= 0.9.10)
- locale (2.1.4)
- logger (1.7.0)
- loofah (2.24.0)
- crass (~> 1.0.2)
- nokogiri (>= 1.12.0)
- lumberjack (1.2.10)
- mail (2.8.1)
- mini_mime (>= 0.1.1)
- net-imap
- net-pop
- net-smtp
- marcel (1.0.4)
- matrix (0.4.2)
- method_source (1.1.0)
- mime-types (3.5.1)
- mime-types-data (~> 3.2015)
- mime-types-data (3.2023.1003)
- mimemagic (0.4.3)
- nokogiri (~> 1)
- rake
- mini_mime (1.1.5)
- minitest (5.25.4)
- mocha (2.7.1)
- ruby2_keywords (>= 0.0.5)
- msgpack (1.7.2)
- multi_json (1.15.0)
- multi_xml (0.7.1)
- bigdecimal (~> 3.1)
- mysql2 (0.5.6)
- nap (1.1.0)
- nenv (0.3.0)
- net-http (0.4.1)
- uri
- net-imap (0.4.20)
- date
- net-protocol
- net-pop (0.1.2)
- net-protocol
- net-protocol (0.2.2)
- timeout
- net-smtp (0.5.0)
- net-protocol
- nio4r (2.7.4)
- no_proxy_fix (0.1.2)
- nokogiri (1.18.9-arm64-darwin)
- racc (~> 1.4)
- nokogiri (1.18.9-x86_64-linux-gnu)
- racc (~> 1.4)
- nokogiri (1.18.9-x86_64-linux-musl)
- racc (~> 1.4)
- notiffany (0.1.3)
- nenv (~> 0.1)
- shellany (~> 0.0)
- oauth2 (2.0.9)
- faraday (>= 0.17.3, < 3.0)
- jwt (>= 1.0, < 3.0)
- multi_xml (~> 0.5)
- rack (>= 1.2, < 4)
- snaky_hash (~> 2.0)
- version_gem (~> 1.1)
- octokit (8.1.0)
- base64
- faraday (>= 1, < 3)
- sawyer (~> 0.9)
- omniauth (2.1.2)
- hashie (>= 3.4.6)
- rack (>= 2.2.3)
- rack-protection
- omniauth-oauth2 (1.8.0)
- oauth2 (>= 1.4, < 3)
- omniauth (~> 2.0)
- omniauth-orcid (2.1.1)
- omniauth-oauth2 (~> 1.3)
- ruby_dig (~> 0.0.2)
- omniauth-rails_csrf_protection (1.0.2)
- actionpack (>= 4.2)
- omniauth (~> 2.0)
- omniauth-shibboleth (1.3.0)
- omniauth (>= 1.0.0)
- omniauth_openid_connect (0.7.1)
- omniauth (>= 1.9, < 3)
- openid_connect (~> 2.2)
- open4 (1.3.4)
- openid_connect (2.3.0)
- activemodel
- attr_required (>= 1.0.0)
- email_validator
- faraday (~> 2.0)
- faraday-follow_redirects
- json-jwt (>= 1.16)
- mail
- rack-oauth2 (~> 2.2)
- swd (~> 2.0)
- tzinfo
- validate_url
- webfinger (~> 2.0)
- options (2.3.2)
- orm_adapter (0.5.0)
- ostruct (0.6.1)
- parallel (1.26.3)
- parser (3.2.2.4)
- ast (~> 2.4.1)
- racc
- pg (1.5.9)
- prime (0.1.4)
- forwardable
- singleton
- progress_bar (1.3.4)
- highline (>= 1.6)
- options (~> 2.3.0)
- pry (0.15.2)
- coderay (~> 1.1)
- method_source (~> 1.0)
- public_suffix (6.0.1)
- puma (6.6.0)
- nio4r (~> 2.0)
- pundit (2.3.2)
- activesupport (>= 3.0.0)
- pundit-matchers (4.0.0)
- rspec-core (~> 3.12)
- rspec-expectations (~> 3.12)
- rspec-mocks (~> 3.12)
- rspec-support (~> 3.12)
- racc (1.8.1)
- rack (2.2.18)
- rack-attack (6.7.0)
- rack (>= 1.0, < 4)
- rack-mini-profiler (3.3.1)
- rack (>= 1.2.0)
- rack-oauth2 (2.2.1)
- activesupport
- attr_required
- faraday (~> 2.0)
- faraday-follow_redirects
- json-jwt (>= 1.11.0)
- rack (>= 2.1.0)
- rack-protection (3.2.0)
- base64 (>= 0.1.0)
- rack (~> 2.2, >= 2.2.4)
- rack-test (2.2.0)
- rack (>= 1.3)
- rails (6.1.7.9)
- actioncable (= 6.1.7.9)
- actionmailbox (= 6.1.7.9)
- actionmailer (= 6.1.7.9)
- actionpack (= 6.1.7.9)
- actiontext (= 6.1.7.9)
- actionview (= 6.1.7.9)
- activejob (= 6.1.7.9)
- activemodel (= 6.1.7.9)
- activerecord (= 6.1.7.9)
- activestorage (= 6.1.7.9)
- activesupport (= 6.1.7.9)
- bundler (>= 1.15.0)
- railties (= 6.1.7.9)
- sprockets-rails (>= 2.0.0)
- rails-controller-testing (1.0.5)
- actionpack (>= 5.0.1.rc1)
- actionview (>= 5.0.1.rc1)
- activesupport (>= 5.0.1.rc1)
- rails-dom-testing (2.2.0)
- activesupport (>= 5.0.0)
- minitest
- nokogiri (>= 1.6)
- rails-html-sanitizer (1.6.2)
- loofah (~> 2.21)
- nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
- railties (6.1.7.9)
- actionpack (= 6.1.7.9)
- activesupport (= 6.1.7.9)
- method_source
- rake (>= 12.2)
- thor (~> 1.0)
- rainbow (3.1.1)
- rake (13.2.1)
- rb-fsevent (0.11.2)
- rb-inotify (0.11.1)
- ffi (~> 1.0)
- rchardet (1.8.0)
- recaptcha (5.18.0)
- regexp_parser (2.8.2)
- reline (0.6.1)
- io-console (~> 0.5)
- responders (3.1.1)
- actionpack (>= 5.2)
- railties (>= 5.2)
- rexml (3.4.2)
- rollbar (3.5.2)
- rouge (4.1.3)
- rspec-collection_matchers (1.2.1)
- rspec-expectations (>= 2.99.0.beta1)
- rspec-core (3.13.2)
- rspec-support (~> 3.13.0)
- rspec-expectations (3.13.3)
- diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.13.0)
- rspec-mocks (3.13.2)
- diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.13.0)
- rspec-rails (6.1.5)
- actionpack (>= 6.1)
- activesupport (>= 6.1)
- railties (>= 6.1)
- rspec-core (~> 3.13)
- rspec-expectations (~> 3.13)
- rspec-mocks (~> 3.13)
- rspec-support (~> 3.13)
- rspec-support (3.13.2)
- rubocop (1.57.1)
- base64 (~> 0.1.1)
- json (~> 2.3)
- language_server-protocol (>= 3.17.0)
- parallel (~> 1.10)
- parser (>= 3.2.2.4)
- rainbow (>= 2.2.2, < 4.0)
- regexp_parser (>= 1.8, < 3.0)
- rexml (>= 3.2.5, < 4.0)
- rubocop-ast (>= 1.28.1, < 2.0)
- ruby-progressbar (~> 1.7)
- unicode-display_width (>= 2.4.0, < 3.0)
- rubocop-ast (1.29.0)
- parser (>= 3.2.1.0)
- rubocop-i18n (3.0.0)
- rubocop (~> 1.0)
- rubocop-performance (1.19.1)
- rubocop (>= 1.7.0, < 2.0)
- rubocop-ast (>= 0.4.0)
- ruby-progressbar (1.13.0)
- ruby2_keywords (0.0.5)
- ruby_dig (0.0.2)
- rubyzip (2.3.2)
- sawyer (0.9.2)
- addressable (>= 2.3.5)
- faraday (>= 0.17.3, < 3)
- selenium-webdriver (4.16.0)
- rexml (~> 3.2, >= 3.2.5)
- rubyzip (>= 1.2.2, < 3.0)
- websocket (~> 1.0)
- shellany (0.0.1)
- shoulda (4.0.0)
- shoulda-context (~> 2.0)
- shoulda-matchers (~> 4.0)
- shoulda-context (2.0.0)
- shoulda-matchers (4.5.1)
- activesupport (>= 4.2.0)
- simpleidn (0.2.1)
- unf (~> 0.1.4)
- singleton (0.3.0)
- snaky_hash (2.0.1)
- hashie
- version_gem (~> 1.1, >= 1.1.1)
- spring (4.2.1)
- spring-commands-rspec (1.0.4)
- spring (>= 0.9.1)
- spring-watcher-listen (2.1.0)
- listen (>= 2.7, < 4.0)
- spring (>= 4)
- sprockets (4.2.1)
- concurrent-ruby (~> 1.0)
- rack (>= 2.2.4, < 4)
- sprockets-rails (3.4.2)
- actionpack (>= 5.2)
- activesupport (>= 5.2)
- sprockets (>= 3.0.0)
- swd (2.0.3)
- activesupport (>= 3)
- attr_required (>= 0.0.5)
- faraday (~> 2.0)
- faraday-follow_redirects
- terminal-table (3.0.2)
- unicode-display_width (>= 1.1.1, < 3)
- text (1.3.1)
- thor (1.4.0)
- timeout (0.4.3)
- tomparse (0.4.2)
- turbo-rails (2.0.5)
- actionpack (>= 6.0.0)
- activejob (>= 6.0.0)
- railties (>= 6.0.0)
- tzinfo (2.0.6)
- concurrent-ruby (~> 1.0)
- unf (0.1.4)
- unf_ext
- unf_ext (0.0.8.2)
- unicode-display_width (2.5.0)
- uniform_notifier (1.16.0)
- uri (0.13.2)
- uri_template (0.7.0)
- validate_url (1.0.15)
- activemodel (>= 3.0.0)
- public_suffix
- version_gem (1.1.3)
- warden (1.2.9)
- rack (>= 2.0.9)
- web-console (4.2.1)
- actionview (>= 6.0.0)
- activemodel (>= 6.0.0)
- bindex (>= 0.4.0)
- railties (>= 6.0.0)
- webfinger (2.1.3)
- activesupport
- faraday (~> 2.0)
- faraday-follow_redirects
- webmock (3.23.1)
- addressable (>= 2.8.0)
- crack (>= 0.3.2)
- hashdiff (>= 0.4.0, < 2.0.0)
- websocket (1.2.10)
- websocket-driver (0.7.6)
- websocket-extensions (>= 0.1.0)
- websocket-extensions (0.1.5)
- wicked_pdf (2.8.2)
- activesupport
- ostruct
- wkhtmltopdf-binary (0.12.6.10)
- xpath (3.2.0)
- nokogiri (~> 1.8)
- yard (0.9.37)
- yard-tomdoc (0.7.1)
- tomparse (>= 0.4.0)
- yard
- zeitwerk (2.6.18)
-
-PLATFORMS
- arm64-darwin-21
- arm64-darwin-22
- x86_64-linux
- x86_64-linux-musl
-
-DEPENDENCIES
- activerecord-nulldb-adapter
- activerecord_json_validator
- annotate
- annotate_gem
- api-pagination
- autoprefixer-rails
- better_errors
- binding_of_caller
- bootsnap
- brakeman
- bullet
- bundle-audit
- byebug
- capybara
- contact_us
- cssbundling-rails
- danger
- database_cleaner
- devise
- devise_invitable
- dotenv-rails
- dragonfly
- dragonfly-s3_data_store
- factory_bot_rails
- faker
- flag_shih_tzu
- fuubar
- guard
- htmltoword
- httparty
- jbuilder
- jsbundling-rails
- jwt
- kaminari
- ledermann-rails-settings
- listen
- mail (= 2.8.1)
- mimemagic
- mocha
- mysql2
- omniauth
- omniauth-orcid
- omniauth-rails_csrf_protection
- omniauth-shibboleth
- omniauth_openid_connect
- parallel
- pg
- progress_bar
- puma
- pundit
- pundit-matchers
- rack-attack (~> 6.6, >= 6.6.1)
- rack-mini-profiler
- rails (~> 6.1)
- rails-controller-testing
- recaptcha
- rollbar
- rspec-collection_matchers
- rspec-rails
- rubocop (= 1.57.1)
- rubocop-i18n
- rubocop-performance
- selenium-webdriver
- shoulda
- spring
- spring-commands-rspec
- spring-watcher-listen
- text
- translation!
- turbo-rails
- web-console
- webmock
- wicked_pdf
- wkhtmltopdf-binary
- yard
- yard-tomdoc
-
-RUBY VERSION
- ruby 3.1.4p223
-
-BUNDLED WITH
- 2.4.15
+GIT
+ remote: https://github.com/ualbertalib/translation_io_rails
+ revision: f60a5427372b51348eb218755e275f0f34d19746
+ specs:
+ translation (1.22)
+ gettext (~> 3.2, >= 3.2.5)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ actioncable (6.1.7.9)
+ actionpack (= 6.1.7.9)
+ activesupport (= 6.1.7.9)
+ nio4r (~> 2.0)
+ websocket-driver (>= 0.6.1)
+ actionmailbox (6.1.7.9)
+ actionpack (= 6.1.7.9)
+ activejob (= 6.1.7.9)
+ activerecord (= 6.1.7.9)
+ activestorage (= 6.1.7.9)
+ activesupport (= 6.1.7.9)
+ mail (>= 2.7.1)
+ actionmailer (6.1.7.9)
+ actionpack (= 6.1.7.9)
+ actionview (= 6.1.7.9)
+ activejob (= 6.1.7.9)
+ activesupport (= 6.1.7.9)
+ mail (~> 2.5, >= 2.5.4)
+ rails-dom-testing (~> 2.0)
+ actionpack (6.1.7.9)
+ actionview (= 6.1.7.9)
+ activesupport (= 6.1.7.9)
+ rack (~> 2.0, >= 2.0.9)
+ rack-test (>= 0.6.3)
+ rails-dom-testing (~> 2.0)
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
+ actiontext (6.1.7.9)
+ actionpack (= 6.1.7.9)
+ activerecord (= 6.1.7.9)
+ activestorage (= 6.1.7.9)
+ activesupport (= 6.1.7.9)
+ nokogiri (>= 1.8.5)
+ actionview (6.1.7.9)
+ activesupport (= 6.1.7.9)
+ builder (~> 3.1)
+ erubi (~> 1.4)
+ rails-dom-testing (~> 2.0)
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
+ activejob (6.1.7.9)
+ activesupport (= 6.1.7.9)
+ globalid (>= 0.3.6)
+ activemodel (6.1.7.9)
+ activesupport (= 6.1.7.9)
+ activerecord (6.1.7.9)
+ activemodel (= 6.1.7.9)
+ activesupport (= 6.1.7.9)
+ activerecord-nulldb-adapter (1.1.1)
+ activerecord (>= 6.0, < 8.1)
+ activerecord_json_validator (2.1.5)
+ activerecord (>= 4.2.0, < 8)
+ json_schemer (~> 0.2.18)
+ activestorage (6.1.7.9)
+ actionpack (= 6.1.7.9)
+ activejob (= 6.1.7.9)
+ activerecord (= 6.1.7.9)
+ activesupport (= 6.1.7.9)
+ marcel (~> 1.0)
+ mini_mime (>= 1.1.0)
+ activesupport (6.1.7.9)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ i18n (>= 1.6, < 2)
+ minitest (>= 5.1)
+ tzinfo (~> 2.0)
+ zeitwerk (~> 2.3)
+ addressable (2.8.7)
+ public_suffix (>= 2.0.2, < 7.0)
+ aes_key_wrap (1.1.0)
+ annotate (3.2.0)
+ activerecord (>= 3.2, < 8.0)
+ rake (>= 10.4, < 14.0)
+ annotate_gem (0.0.14)
+ bundler (>= 1.1)
+ api-pagination (5.0.0)
+ ast (2.4.2)
+ attr_required (1.0.2)
+ autoprefixer-rails (10.4.19.0)
+ execjs (~> 2)
+ base64 (0.1.2)
+ bcrypt (3.1.20)
+ better_errors (2.10.1)
+ erubi (>= 1.0.0)
+ rack (>= 0.9.0)
+ rouge (>= 1.0.0)
+ bigdecimal (3.3.1)
+ bindata (2.5.0)
+ bindex (0.8.1)
+ binding_of_caller (1.0.1)
+ debug_inspector (>= 1.2.0)
+ bootsnap (1.18.3)
+ msgpack (~> 1.2)
+ brakeman (7.0.0)
+ racc
+ builder (3.3.0)
+ bullet (7.1.6)
+ activesupport (>= 3.0.0)
+ uniform_notifier (~> 1.11)
+ bundle-audit (0.1.0)
+ bundler-audit
+ bundler-audit (0.9.1)
+ bundler (>= 1.2.0, < 3)
+ thor (~> 1.0)
+ byebug (11.1.3)
+ capybara (3.39.2)
+ addressable
+ matrix
+ mini_mime (>= 0.1.3)
+ nokogiri (~> 1.8)
+ rack (>= 1.6.0)
+ rack-test (>= 0.6.3)
+ regexp_parser (>= 1.5, < 3.0)
+ xpath (~> 3.2)
+ claide (1.1.0)
+ claide-plugins (0.9.2)
+ cork
+ nap
+ open4 (~> 1.3)
+ coderay (1.1.3)
+ colored2 (3.1.2)
+ concurrent-ruby (1.3.4)
+ contact_us (1.2.0)
+ rails (>= 4.2.0)
+ cork (0.3.0)
+ colored2 (~> 3.1)
+ crack (1.0.0)
+ bigdecimal
+ rexml
+ crass (1.0.6)
+ cssbundling-rails (1.4.1)
+ railties (>= 6.0.0)
+ csv (3.3.5)
+ danger (9.4.3)
+ claide (~> 1.0)
+ claide-plugins (>= 0.9.2)
+ colored2 (~> 3.1)
+ cork (~> 0.1)
+ faraday (>= 0.9.0, < 3.0)
+ faraday-http-cache (~> 2.0)
+ git (~> 1.13)
+ kramdown (~> 2.3)
+ kramdown-parser-gfm (~> 1.0)
+ no_proxy_fix
+ octokit (>= 4.0)
+ terminal-table (>= 1, < 4)
+ database_cleaner (2.0.2)
+ database_cleaner-active_record (>= 2, < 3)
+ database_cleaner-active_record (2.1.0)
+ activerecord (>= 5.a)
+ database_cleaner-core (~> 2.0.0)
+ database_cleaner-core (2.0.1)
+ date (3.4.1)
+ debug_inspector (1.2.0)
+ devise (4.9.4)
+ bcrypt (~> 3.0)
+ orm_adapter (~> 0.1)
+ railties (>= 4.1.0)
+ responders
+ warden (~> 1.2.3)
+ devise_invitable (2.0.9)
+ actionmailer (>= 5.0)
+ devise (>= 4.6)
+ diff-lcs (1.5.1)
+ doorkeeper (5.8.2)
+ railties (>= 5)
+ dotenv (2.8.1)
+ dotenv-rails (2.8.1)
+ dotenv (= 2.8.1)
+ railties (>= 3.2)
+ dragonfly (1.4.1)
+ addressable (~> 2.3)
+ multi_json (~> 1.0)
+ ostruct (~> 0.6.1)
+ rack (>= 1.3)
+ dragonfly-s3_data_store (1.3.0)
+ dragonfly (~> 1.0)
+ fog-aws
+ ecma-re-validator (0.4.0)
+ regexp_parser (~> 2.2)
+ email_validator (2.2.4)
+ activemodel
+ erubi (1.13.1)
+ excon (0.104.0)
+ execjs (2.10.0)
+ factory_bot (6.2.1)
+ activesupport (>= 5.0.0)
+ factory_bot_rails (6.2.0)
+ factory_bot (~> 6.2.0)
+ railties (>= 5.0.0)
+ faker (3.4.1)
+ i18n (>= 1.8.11, < 2)
+ faraday (2.9.0)
+ faraday-net_http (>= 2.0, < 3.2)
+ faraday-follow_redirects (0.3.0)
+ faraday (>= 1, < 3)
+ faraday-http-cache (2.5.1)
+ faraday (>= 0.8)
+ faraday-net_http (3.1.0)
+ net-http
+ ffi (1.17.2-arm64-darwin)
+ ffi (1.17.2-x86_64-linux-gnu)
+ ffi (1.17.2-x86_64-linux-musl)
+ flag_shih_tzu (0.3.23)
+ fog-aws (3.21.0)
+ fog-core (~> 2.1)
+ fog-json (~> 1.1)
+ fog-xml (~> 0.1)
+ fog-core (2.3.0)
+ builder
+ excon (~> 0.71)
+ formatador (>= 0.2, < 2.0)
+ mime-types
+ fog-json (1.2.0)
+ fog-core
+ multi_json (~> 1.10)
+ fog-xml (0.1.4)
+ fog-core
+ nokogiri (>= 1.5.11, < 2.0.0)
+ formatador (1.1.0)
+ forwardable (1.3.3)
+ fuubar (2.5.1)
+ rspec-core (~> 3.0)
+ ruby-progressbar (~> 1.4)
+ gettext (3.5.1)
+ erubi
+ locale (>= 2.0.5)
+ prime
+ racc
+ text (>= 1.3.0)
+ git (1.19.1)
+ addressable (~> 2.8)
+ rchardet (~> 1.8)
+ globalid (1.2.1)
+ activesupport (>= 6.1)
+ guard (2.19.1)
+ formatador (>= 0.2.4)
+ listen (>= 2.7, < 4.0)
+ logger (~> 1.6)
+ lumberjack (>= 1.0.12, < 2.0)
+ nenv (~> 0.1)
+ notiffany (~> 0.0)
+ ostruct (~> 0.6)
+ pry (>= 0.13.0)
+ shellany (~> 0.0)
+ thor (>= 0.18.1)
+ hana (1.3.7)
+ hashdiff (1.1.0)
+ hashie (5.0.0)
+ highline (3.1.2)
+ reline
+ htmltoword (1.1.1)
+ actionpack
+ nokogiri
+ rubyzip (>= 1.0)
+ httparty (0.24.2)
+ csv
+ mini_mime (>= 1.0.0)
+ multi_xml (>= 0.5.2)
+ i18n (1.14.6)
+ concurrent-ruby (~> 1.0)
+ io-console (0.8.0)
+ jbuilder (2.12.0)
+ actionview (>= 5.0.0)
+ activesupport (>= 5.0.0)
+ jsbundling-rails (1.3.0)
+ railties (>= 6.0.0)
+ json (2.7.2)
+ json-jwt (1.16.6)
+ activesupport (>= 4.2)
+ aes_key_wrap
+ base64
+ bindata
+ faraday (~> 2.0)
+ faraday-follow_redirects
+ json_schemer (0.2.25)
+ ecma-re-validator (~> 0.3)
+ hana (~> 1.3)
+ regexp_parser (~> 2.0)
+ simpleidn (~> 0.2)
+ uri_template (~> 0.7)
+ jwt (2.10.1)
+ base64
+ kaminari (1.2.2)
+ activesupport (>= 4.1.0)
+ kaminari-actionview (= 1.2.2)
+ kaminari-activerecord (= 1.2.2)
+ kaminari-core (= 1.2.2)
+ kaminari-actionview (1.2.2)
+ actionview
+ kaminari-core (= 1.2.2)
+ kaminari-activerecord (1.2.2)
+ activerecord
+ kaminari-core (= 1.2.2)
+ kaminari-core (1.2.2)
+ kramdown (2.4.0)
+ rexml
+ kramdown-parser-gfm (1.1.0)
+ kramdown (~> 2.0)
+ language_server-protocol (3.17.0.3)
+ ledermann-rails-settings (2.6.2)
+ activerecord (>= 6.1)
+ listen (3.9.0)
+ rb-fsevent (~> 0.10, >= 0.10.3)
+ rb-inotify (~> 0.9, >= 0.9.10)
+ locale (2.1.4)
+ logger (1.7.0)
+ loofah (2.24.0)
+ crass (~> 1.0.2)
+ nokogiri (>= 1.12.0)
+ lumberjack (1.2.10)
+ mail (2.8.1)
+ mini_mime (>= 0.1.1)
+ net-imap
+ net-pop
+ net-smtp
+ marcel (1.0.4)
+ matrix (0.4.2)
+ method_source (1.1.0)
+ mime-types (3.5.1)
+ mime-types-data (~> 3.2015)
+ mime-types-data (3.2023.1003)
+ mimemagic (0.4.3)
+ nokogiri (~> 1)
+ rake
+ mini_mime (1.1.5)
+ minitest (5.25.4)
+ mocha (2.7.1)
+ ruby2_keywords (>= 0.0.5)
+ msgpack (1.7.2)
+ multi_json (1.15.0)
+ multi_xml (0.7.1)
+ bigdecimal (~> 3.1)
+ mysql2 (0.5.6)
+ nap (1.1.0)
+ nenv (0.3.0)
+ net-http (0.4.1)
+ uri
+ net-imap (0.4.20)
+ date
+ net-protocol
+ net-pop (0.1.2)
+ net-protocol
+ net-protocol (0.2.2)
+ timeout
+ net-smtp (0.5.0)
+ net-protocol
+ nio4r (2.7.4)
+ no_proxy_fix (0.1.2)
+ nokogiri (1.18.9-arm64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.18.9-x86_64-linux-gnu)
+ racc (~> 1.4)
+ nokogiri (1.18.9-x86_64-linux-musl)
+ racc (~> 1.4)
+ notiffany (0.1.3)
+ nenv (~> 0.1)
+ shellany (~> 0.0)
+ oauth2 (2.0.9)
+ faraday (>= 0.17.3, < 3.0)
+ jwt (>= 1.0, < 3.0)
+ multi_xml (~> 0.5)
+ rack (>= 1.2, < 4)
+ snaky_hash (~> 2.0)
+ version_gem (~> 1.1)
+ octokit (8.1.0)
+ base64
+ faraday (>= 1, < 3)
+ sawyer (~> 0.9)
+ omniauth (2.1.4)
+ hashie (>= 3.4.6)
+ logger
+ rack (>= 2.2.3)
+ rack-protection
+ omniauth-oauth2 (1.8.0)
+ oauth2 (>= 1.4, < 3)
+ omniauth (~> 2.0)
+ omniauth-orcid (2.1.1)
+ omniauth-oauth2 (~> 1.3)
+ ruby_dig (~> 0.0.2)
+ omniauth-rails_csrf_protection (1.0.2)
+ actionpack (>= 4.2)
+ omniauth (~> 2.0)
+ omniauth-shibboleth (1.3.0)
+ omniauth (>= 1.0.0)
+ omniauth_openid_connect (0.7.1)
+ omniauth (>= 1.9, < 3)
+ openid_connect (~> 2.2)
+ open4 (1.3.4)
+ openid_connect (2.3.0)
+ activemodel
+ attr_required (>= 1.0.0)
+ email_validator
+ faraday (~> 2.0)
+ faraday-follow_redirects
+ json-jwt (>= 1.16)
+ mail
+ rack-oauth2 (~> 2.2)
+ swd (~> 2.0)
+ tzinfo
+ validate_url
+ webfinger (~> 2.0)
+ options (2.3.2)
+ orm_adapter (0.5.0)
+ ostruct (0.6.1)
+ parallel (1.26.3)
+ parser (3.2.2.4)
+ ast (~> 2.4.1)
+ racc
+ pg (1.5.9)
+ prime (0.1.4)
+ forwardable
+ singleton
+ progress_bar (1.3.4)
+ highline (>= 1.6)
+ options (~> 2.3.0)
+ pry (0.15.2)
+ coderay (~> 1.1)
+ method_source (~> 1.0)
+ public_suffix (6.0.1)
+ puma (6.6.0)
+ nio4r (~> 2.0)
+ pundit (2.3.2)
+ activesupport (>= 3.0.0)
+ pundit-matchers (4.0.0)
+ rspec-core (~> 3.12)
+ rspec-expectations (~> 3.12)
+ rspec-mocks (~> 3.12)
+ rspec-support (~> 3.12)
+ racc (1.8.1)
+ rack (2.2.21)
+ rack-attack (6.7.0)
+ rack (>= 1.0, < 4)
+ rack-mini-profiler (3.3.1)
+ rack (>= 1.2.0)
+ rack-oauth2 (2.2.1)
+ activesupport
+ attr_required
+ faraday (~> 2.0)
+ faraday-follow_redirects
+ json-jwt (>= 1.11.0)
+ rack (>= 2.1.0)
+ rack-protection (3.2.0)
+ base64 (>= 0.1.0)
+ rack (~> 2.2, >= 2.2.4)
+ rack-test (2.2.0)
+ rack (>= 1.3)
+ rails (6.1.7.9)
+ actioncable (= 6.1.7.9)
+ actionmailbox (= 6.1.7.9)
+ actionmailer (= 6.1.7.9)
+ actionpack (= 6.1.7.9)
+ actiontext (= 6.1.7.9)
+ actionview (= 6.1.7.9)
+ activejob (= 6.1.7.9)
+ activemodel (= 6.1.7.9)
+ activerecord (= 6.1.7.9)
+ activestorage (= 6.1.7.9)
+ activesupport (= 6.1.7.9)
+ bundler (>= 1.15.0)
+ railties (= 6.1.7.9)
+ sprockets-rails (>= 2.0.0)
+ rails-controller-testing (1.0.5)
+ actionpack (>= 5.0.1.rc1)
+ actionview (>= 5.0.1.rc1)
+ activesupport (>= 5.0.1.rc1)
+ rails-dom-testing (2.2.0)
+ activesupport (>= 5.0.0)
+ minitest
+ nokogiri (>= 1.6)
+ rails-html-sanitizer (1.6.2)
+ loofah (~> 2.21)
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
+ railties (6.1.7.9)
+ actionpack (= 6.1.7.9)
+ activesupport (= 6.1.7.9)
+ method_source
+ rake (>= 12.2)
+ thor (~> 1.0)
+ rainbow (3.1.1)
+ rake (13.2.1)
+ rb-fsevent (0.11.2)
+ rb-inotify (0.11.1)
+ ffi (~> 1.0)
+ rchardet (1.8.0)
+ recaptcha (5.18.0)
+ regexp_parser (2.8.2)
+ reline (0.6.1)
+ io-console (~> 0.5)
+ responders (3.1.1)
+ actionpack (>= 5.2)
+ railties (>= 5.2)
+ rexml (3.4.2)
+ rollbar (3.5.2)
+ rouge (4.1.3)
+ rspec-collection_matchers (1.2.1)
+ rspec-expectations (>= 2.99.0.beta1)
+ rspec-core (3.13.2)
+ rspec-support (~> 3.13.0)
+ rspec-expectations (3.13.3)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.13.0)
+ rspec-mocks (3.13.2)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.13.0)
+ rspec-rails (6.1.5)
+ actionpack (>= 6.1)
+ activesupport (>= 6.1)
+ railties (>= 6.1)
+ rspec-core (~> 3.13)
+ rspec-expectations (~> 3.13)
+ rspec-mocks (~> 3.13)
+ rspec-support (~> 3.13)
+ rspec-support (3.13.2)
+ rubocop (1.57.1)
+ base64 (~> 0.1.1)
+ json (~> 2.3)
+ language_server-protocol (>= 3.17.0)
+ parallel (~> 1.10)
+ parser (>= 3.2.2.4)
+ rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 1.8, < 3.0)
+ rexml (>= 3.2.5, < 4.0)
+ rubocop-ast (>= 1.28.1, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 2.4.0, < 3.0)
+ rubocop-ast (1.29.0)
+ parser (>= 3.2.1.0)
+ rubocop-i18n (3.0.0)
+ rubocop (~> 1.0)
+ rubocop-performance (1.19.1)
+ rubocop (>= 1.7.0, < 2.0)
+ rubocop-ast (>= 0.4.0)
+ ruby-progressbar (1.13.0)
+ ruby2_keywords (0.0.5)
+ ruby_dig (0.0.2)
+ rubyzip (2.3.2)
+ sawyer (0.9.2)
+ addressable (>= 2.3.5)
+ faraday (>= 0.17.3, < 3)
+ selenium-webdriver (4.16.0)
+ rexml (~> 3.2, >= 3.2.5)
+ rubyzip (>= 1.2.2, < 3.0)
+ websocket (~> 1.0)
+ shellany (0.0.1)
+ shoulda (4.0.0)
+ shoulda-context (~> 2.0)
+ shoulda-matchers (~> 4.0)
+ shoulda-context (2.0.0)
+ shoulda-matchers (4.5.1)
+ activesupport (>= 4.2.0)
+ simpleidn (0.2.1)
+ unf (~> 0.1.4)
+ singleton (0.3.0)
+ snaky_hash (2.0.1)
+ hashie
+ version_gem (~> 1.1, >= 1.1.1)
+ spring (4.2.1)
+ spring-commands-rspec (1.0.4)
+ spring (>= 0.9.1)
+ spring-watcher-listen (2.1.0)
+ listen (>= 2.7, < 4.0)
+ spring (>= 4)
+ sprockets (4.2.1)
+ concurrent-ruby (~> 1.0)
+ rack (>= 2.2.4, < 4)
+ sprockets-rails (3.4.2)
+ actionpack (>= 5.2)
+ activesupport (>= 5.2)
+ sprockets (>= 3.0.0)
+ swd (2.0.3)
+ activesupport (>= 3)
+ attr_required (>= 0.0.5)
+ faraday (~> 2.0)
+ faraday-follow_redirects
+ terminal-table (3.0.2)
+ unicode-display_width (>= 1.1.1, < 3)
+ text (1.3.1)
+ thor (1.4.0)
+ timeout (0.4.3)
+ tomparse (0.4.2)
+ turbo-rails (2.0.5)
+ actionpack (>= 6.0.0)
+ activejob (>= 6.0.0)
+ railties (>= 6.0.0)
+ tzinfo (2.0.6)
+ concurrent-ruby (~> 1.0)
+ unf (0.1.4)
+ unf_ext
+ unf_ext (0.0.8.2)
+ unicode-display_width (2.5.0)
+ uniform_notifier (1.16.0)
+ uri (0.13.2)
+ uri_template (0.7.0)
+ validate_url (1.0.15)
+ activemodel (>= 3.0.0)
+ public_suffix
+ version_gem (1.1.3)
+ warden (1.2.9)
+ rack (>= 2.0.9)
+ web-console (4.2.1)
+ actionview (>= 6.0.0)
+ activemodel (>= 6.0.0)
+ bindex (>= 0.4.0)
+ railties (>= 6.0.0)
+ webfinger (2.1.3)
+ activesupport
+ faraday (~> 2.0)
+ faraday-follow_redirects
+ webmock (3.23.1)
+ addressable (>= 2.8.0)
+ crack (>= 0.3.2)
+ hashdiff (>= 0.4.0, < 2.0.0)
+ websocket (1.2.10)
+ websocket-driver (0.7.6)
+ websocket-extensions (>= 0.1.0)
+ websocket-extensions (0.1.5)
+ wicked_pdf (2.8.2)
+ activesupport
+ ostruct
+ wkhtmltopdf-binary (0.12.6.10)
+ xpath (3.2.0)
+ nokogiri (~> 1.8)
+ yard (0.9.37)
+ yard-tomdoc (0.7.1)
+ tomparse (>= 0.4.0)
+ yard
+ zeitwerk (2.6.18)
+
+PLATFORMS
+ arm64-darwin-21
+ arm64-darwin-22
+ x86_64-linux
+ x86_64-linux-musl
+
+DEPENDENCIES
+ activerecord-nulldb-adapter
+ activerecord_json_validator
+ annotate
+ annotate_gem
+ api-pagination
+ autoprefixer-rails
+ better_errors
+ binding_of_caller
+ bootsnap
+ brakeman
+ bullet
+ bundle-audit
+ byebug
+ capybara
+ contact_us
+ cssbundling-rails
+ danger
+ database_cleaner
+ devise
+ devise_invitable
+ doorkeeper
+ dotenv-rails
+ dragonfly
+ dragonfly-s3_data_store
+ factory_bot_rails
+ faker
+ flag_shih_tzu
+ fuubar
+ guard
+ htmltoword
+ httparty
+ jbuilder
+ jsbundling-rails
+ jwt
+ kaminari
+ ledermann-rails-settings
+ listen
+ mail (= 2.8.1)
+ mimemagic
+ mocha
+ mysql2
+ omniauth
+ omniauth-orcid
+ omniauth-rails_csrf_protection
+ omniauth-shibboleth
+ omniauth_openid_connect
+ parallel
+ pg
+ progress_bar
+ puma
+ pundit
+ pundit-matchers
+ rack-attack (~> 6.6, >= 6.6.1)
+ rack-mini-profiler
+ rails (~> 6.1)
+ rails-controller-testing
+ recaptcha
+ rollbar
+ rspec-collection_matchers
+ rspec-rails
+ rubocop (= 1.57.1)
+ rubocop-i18n
+ rubocop-performance
+ selenium-webdriver
+ shoulda
+ spring
+ spring-commands-rspec
+ spring-watcher-listen
+ text
+ translation!
+ turbo-rails
+ web-console
+ webmock
+ wicked_pdf
+ wkhtmltopdf-binary
+ yard
+ yard-tomdoc
+
+RUBY VERSION
+ ruby 3.1.4p223
+
+BUNDLED WITH
+ 2.4.15
diff --git a/Rakefile b/Rakefile
index 42f9525d64..16a081e804 100755
--- a/Rakefile
+++ b/Rakefile
@@ -13,5 +13,6 @@
require_relative 'config/application'
DMPRoadmap::Application.load_tasks
+Doorkeeper::Rake.load_tasks
task default: :test
diff --git a/app/assets/images/available.png b/app/assets/images/available.png
new file mode 100644
index 0000000000..24c044dc41
Binary files /dev/null and b/app/assets/images/available.png differ
diff --git a/app/assets/images/collaborative.png b/app/assets/images/collaborative.png
new file mode 100644
index 0000000000..b0b6c71e56
Binary files /dev/null and b/app/assets/images/collaborative.png differ
diff --git a/app/assets/images/lifecycle.png b/app/assets/images/lifecycle.png
new file mode 100644
index 0000000000..160519d756
Binary files /dev/null and b/app/assets/images/lifecycle.png differ
diff --git a/app/assets/stylesheets/dmp-assistant/blocks/_index.scss b/app/assets/stylesheets/dmp-assistant/blocks/_index.scss
index 6fd3e7693b..fa638ff2eb 100644
--- a/app/assets/stylesheets/dmp-assistant/blocks/_index.scss
+++ b/app/assets/stylesheets/dmp-assistant/blocks/_index.scss
@@ -1,2 +1,3 @@
@use 'password_checklist';
-@use 'template_dropdown_menu';
\ No newline at end of file
+@use 'template_dropdown_menu';
+@use 'landing';
\ No newline at end of file
diff --git a/app/assets/stylesheets/dmp-assistant/blocks/_landing.scss b/app/assets/stylesheets/dmp-assistant/blocks/_landing.scss
new file mode 100644
index 0000000000..3218010a6a
--- /dev/null
+++ b/app/assets/stylesheets/dmp-assistant/blocks/_landing.scss
@@ -0,0 +1,56 @@
+@use '../variables/colours' as *;
+
+.custom-accordion {
+
+ summary {
+ cursor: pointer;
+ padding: 16px 20px 16px 32px;
+ font-weight: 600;
+ font-size: 18px;
+ color: $color-alliance-digital-grey;
+ position: relative;
+
+ &::before {
+ content: "▸";
+ position: absolute;
+ left: 16px;
+ transition: transform 0.2s ease;
+ }
+ }
+
+ details[open] summary::before {
+ transform: rotate(90deg);
+ }
+
+ details > *:not(summary) {
+ padding: 0 20px 20px 40px;
+ background: $color-white;
+ }
+}
+
+.welcome-features {
+ margin: 30px 0;
+ padding: 30px;
+ display: flex;
+ justify-content: space-between;
+ gap: 40px;
+ text-align: center;
+
+ .feature {
+ flex: 1;
+
+ .feature-icon {
+ margin-bottom: 15px;
+
+ img {
+ height: 70px;
+ width: auto;
+ }
+ }
+
+ p {
+ font-weight: 600;
+ color: $color-alliance-digital-grey;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/controllers/api/v2/base_api_controller.rb b/app/controllers/api/v2/base_api_controller.rb
new file mode 100644
index 0000000000..fa4aad2b79
--- /dev/null
+++ b/app/controllers/api/v2/base_api_controller.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ class BaseApiController < ApplicationController # rubocop:todo Style/Documentation
+ # skipping the standard rails authenticity tokens passed in the UI
+ skip_before_action :verify_authenticity_token
+
+ # call doorkeeper to authorize the request
+ before_action :doorkeeper_authorize!, except: %i[heartbeat]
+ # get details of server (e.g. DMPonline) and client app
+ before_action :base_response_content
+
+ before_action :log_access
+
+ before_action :require_read_scope, except: %i[heartbeat me]
+ # controller can respond to json format requests
+ respond_to :json
+
+ # set up pages in response
+ before_action :pagination_params, except: %i[heartbeat]
+
+ rescue_from StandardError, with: :handle_exception
+
+ # GET /api/v2/heartbeat
+ def heartbeat
+ render '/api/v2/heartbeat'
+ end
+
+ # GET /me.json - recommended for doorkeeper gem
+ def me
+ render json: @resource_owner.slice(:firstname, :surname, :email).merge(
+ organisation: @resource_owner.org.name,
+ language: @resource_owner.language&.name
+ )
+ end
+
+ private
+
+ # define instance variable json and associated getter and setter methods
+ attr_accessor :json
+
+ def base_response_content
+ @application = ApplicationService.application_name
+ @client = doorkeeper_token&.application
+ @caller = @client&.name || request.remote_ip
+ return unless doorkeeper_token&.resource_owner_id
+
+ @resource_owner = User.find(doorkeeper_token.resource_owner_id)
+ end
+
+ def log_access
+ if @client.present?
+ Rails.logger.info "Client (OAuth) application name: #{@client.name}"
+ Rails.logger.info "Client (OAuth) application uid: #{@client.uid}"
+ end
+ Rails.logger.info "Resource owner id: #{@resource_owner.id}" if @resource_owner
+ end
+
+ def handle_exception(exception)
+ if exception.is_a?(Pundit::NotAuthorizedError)
+ handle_client_not_authorized
+ else
+ handle_internal_server_error(exception)
+ end
+ end
+
+ def handle_internal_server_error(exception)
+ # log server errors
+ Rails.logger.error "Exception message: #{exception.message}"
+
+ # inform client of server error
+ message = _('There was a problem in the server.')
+ @payload = { message: [message] }
+ render '/api/v2/error', status: :internal_server_error
+ end
+
+ def handle_client_not_authorized
+ message = _('The client is not authorized to perform this action.')
+ @payload = { message: [message] }
+ render '/api/v2/error', status: :forbidden
+ end
+
+ # retrieve the requested pagination params or use defaults
+ # only allow 100 per page as the max
+ def pagination_params
+ max_per_page = Rails.configuration.x.application.api_max_page_size
+ @page = params.fetch('page', 1).to_i
+ @per_page = params.fetch('per_page', max_per_page).to_i
+ @per_page = max_per_page if @per_page > max_per_page
+ end
+
+ def paginate_response(results:)
+ results = results.page(@page).per(@per_page)
+ @total_items = results.total_count
+ results
+ end
+
+ def require_read_scope
+ raise Pundit::NotAuthorizedError unless doorkeeper_token.scopes.include?('read')
+ end
+ end
+ end
+end
diff --git a/app/controllers/api/v2/internal_user_access_tokens_controller.rb b/app/controllers/api/v2/internal_user_access_tokens_controller.rb
new file mode 100644
index 0000000000..6cb19454ec
--- /dev/null
+++ b/app/controllers/api/v2/internal_user_access_tokens_controller.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ # Controller for managing the current user's internal V2 API access token.
+ # Provides token rotation for authenticated internal users.
+ # See Api::V2::InternalUserAccessTokenService for token implementation details.
+ class InternalUserAccessTokensController < ApplicationController
+ # POST "/api/v2/internal_user_access_token"
+ def create
+ authorize current_user, :internal_user_v2_access_token?
+ @v2_token = Api::V2::InternalUserAccessTokenService.rotate!(current_user)
+ @success = true
+ respond_to do |format|
+ format.js { render 'users/refresh_token' }
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/api/v2/plans_controller.rb b/app/controllers/api/v2/plans_controller.rb
new file mode 100644
index 0000000000..ba4c507bb6
--- /dev/null
+++ b/app/controllers/api/v2/plans_controller.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ class PlansController < BaseApiController # rubocop:todo Style/Documentation
+ respond_to :json
+ before_action :set_complete_param, only: %i[show index]
+
+ # GET /api/v2/plans/:id
+ def show
+ @plan = plans_scope.find_by(id: params[:id])
+
+ plans_policy = PlansPolicy.new(@resource_owner, @plan)
+ raise Pundit::NotAuthorizedError unless plans_policy.show?
+
+ @items = [@plan]
+ render '/api/v2/plans/index', status: :ok
+ end
+
+ # GET /api/v2/plans
+ def index
+ @plans = plans_scope
+ @items = paginate_response(results: @plans)
+ render '/api/v2/plans/index', status: :ok
+ end
+
+ private
+
+ # GET /api/v2/plans?complete=true and /api/v2/plans/:id?complete=true
+ def set_complete_param
+ @complete = params[:complete].to_s.casecmp('true').zero?
+ end
+
+ def plans_scope
+ scope = PlansPolicy::Scope.new(@resource_owner).resolve
+ @complete ? scope.includes(answers: { question: :section }) : scope
+ end
+ end
+ end
+end
diff --git a/app/controllers/api/v2/templates_controller.rb b/app/controllers/api/v2/templates_controller.rb
new file mode 100644
index 0000000000..2b9a4bf4bb
--- /dev/null
+++ b/app/controllers/api/v2/templates_controller.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ # provides a list of templates for API V2
+ class TemplatesController < BaseApiController
+ respond_to :json
+
+ # GET /api/v2/templates
+ def index
+ templates = Api::V2::TemplatesPolicy::Scope.new(@resource_owner).resolve
+ @items = paginate_response(results: templates)
+ render '/api/v2/templates/index', status: :ok
+ end
+ end
+ end
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index cc3c422974..65b1ca6400 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -61,28 +61,25 @@ def store_location
end
end
- # rubocop:disable Metrics/AbcSize
def after_sign_in_path_for(_resource)
- referer_path = URI(request.referer).path unless request.referer.nil?
- if from_external_domain? || referer_path.eql?(new_user_session_path) ||
- referer_path.eql?(new_user_registration_path) ||
- referer_path.nil?
- root_path
- else
- request.referer
- end
+ after_auth_path(disallowed_paths: [new_user_session_path, new_user_registration_path])
end
- # rubocop:enable Metrics/AbcSize
def after_sign_up_path_for(_resource)
- referer_path = URI(request.referer).path unless request.referer.nil?
- if from_external_domain? ||
- referer_path.eql?(new_user_session_path) ||
- referer_path.nil?
- root_path
- else
- request.referer
- end
+ after_auth_path(disallowed_paths: [new_user_session_path])
+ end
+
+ def after_auth_path(disallowed_paths:)
+ # ensure oauth2 authorization flow is not interrupted
+ # TODO: Unless nil, should stored_location_for(resource) always be returned?
+ return stored_location_for(:user) if user_is_in_oauth_flow?
+
+ return root_path if request.referer.nil? || from_external_domain?
+
+ referer_path = URI(request.referer).path
+ return root_path if disallowed_paths.include?(referer_path)
+
+ request.referer
end
def after_sign_in_error_path_for(_resource)
@@ -194,4 +191,8 @@ def render_respond_to_format_with_error_message(msg, url_or_path, http_status, e
end
end
end
+
+ def user_is_in_oauth_flow?
+ session[:user_return_to]&.start_with?(oauth_authorization_path)
+ end
end
diff --git a/app/controllers/concerns/plan_permitted_params.rb b/app/controllers/concerns/plan_permitted_params.rb
new file mode 100644
index 0000000000..90363f9de8
--- /dev/null
+++ b/app/controllers/concerns/plan_permitted_params.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+module PlanPermittedParams # rubocop:todo Metrics/ModuleLength, Style/Documentation
+ extend ActiveSupport::Concern
+
+ def plan_permitted_params
+ [
+ :created,
+ :title,
+ :description,
+ :language,
+ :ethical_issues_exist,
+ :ethical_issues_description,
+ :ethical_issues_report,
+ { dmp_ids: identifier_permitted_params },
+ { contact: contributor_permitted_params },
+ { contributors: contributor_permitted_params },
+ { costs: cost_permitted_params },
+ { project: project_permitted_params },
+ { datasets: dataset_permitted_params }
+ ]
+ end
+
+ def identifier_permitted_params
+ %i[
+ type
+ identifier
+ ]
+ end
+
+ def contributor_permitted_params
+ [
+ :firstname,
+ :surname,
+ :mbox,
+ :role,
+ { affiliations: affiliation_permitted_params },
+ { contributor_ids: identifier_permitted_params }
+ ]
+ end
+
+ def affiliation_permitted_params
+ [
+ :name,
+ :abbreviation,
+ { affiliation_ids: identifier_permitted_params }
+ ]
+ end
+
+ def cost_permitted_params
+ %i[
+ title
+ description
+ value
+ currency_code
+ ]
+ end
+
+ def project_permitted_params
+ [
+ :title,
+ :description,
+ :start_on,
+ :end_on,
+ { funding: funding_permitted_params }
+ ]
+ end
+
+ def funding_permitted_params
+ [
+ :name,
+ :funding_status,
+ { funder_ids: identifier_permitted_params },
+ { grant_ids: identifier_permitted_params }
+ ]
+ end
+
+ def dataset_permitted_params
+ [
+ :title,
+ :doi_url,
+ :description,
+ :type,
+ :issued,
+ :language,
+ :personal_data,
+ :sensitive_data,
+ :keywords,
+ :data_quality_assurance,
+ :preservation_statement,
+ { dataset_ids: identifier_permitted_params },
+ { metadata: metadatum_permitted_params },
+ { security_and_privacy_statements: security_and_privacy_statement_permitted_params },
+ { technical_resources: technical_resource_permitted_params },
+ { distributions: distribution_permitted_params }
+ ]
+ end
+
+ def metadatum_permitted_params
+ [
+ :description,
+ :language,
+ { identifier: identifier_permitted_params }
+ ]
+ end
+
+ def security_and_privacy_statement_permitted_params
+ %i[
+ title
+ description
+ ]
+ end
+
+ def technical_resource_permitted_params
+ [
+ :description,
+ { identifier: identifier_permitted_params }
+ ]
+ end
+
+ def distribution_permitted_params
+ [
+ :title,
+ :description,
+ :format,
+ :byte_size,
+ :access_url,
+ :download_url,
+ :data_access,
+ :available_until,
+ { licenses: license_permitted_params },
+ { host: host_permitted_params }
+ ]
+ end
+
+ def license_permitted_params
+ %i[
+ license_ref
+ start_date
+ ]
+ end
+
+ def host_permitted_params
+ [
+ :title,
+ :description,
+ :supports_versioning,
+ :backup_type,
+ :backup_frequency,
+ :storage_type,
+ :availability,
+ :geo_location,
+ :certified_with,
+ :pid_system,
+ { host_ids: identifier_permitted_params }
+ ]
+ end
+end
diff --git a/app/controllers/plan_exports_controller.rb b/app/controllers/plan_exports_controller.rb
index 0ec8174d42..043d15d113 100644
--- a/app/controllers/plan_exports_controller.rb
+++ b/app/controllers/plan_exports_controller.rb
@@ -114,8 +114,11 @@ def show_pdf
end
def show_json
- json = render_to_string(partial: '/api/v1/plans/show', locals: { plan: @plan })
- render json: "{\"dmp\":#{json}}"
+ @complete = true
+ @plan = Plan.for_api_v2(current_user.id)
+ .find_by(id: @plan.id)
+ @items = [@plan]
+ render '/api/v2/plans/index', formats: :json
end
def file_name
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 5cc1633bc9..bfaa2973dd 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -53,24 +53,6 @@ def current_locale_logo
end
end
- def how_to_manage_your_data_path
- if I18n.locale == :'fr-CA'
- 'https://portagenetwork.ca/fr/outils-et-ressources/assistant-pgd/comment-gerer-vos-donnees/'
- else
- # Handling :'en-CA' locale
- 'https://portagenetwork.ca/tools-and-resources/dmp-assistant/how-to-manage-your-data/'
- end
- end
-
- def contacts_at_your_instutution_path
- if I18n.locale == :'fr-CA'
- 'https://alliancecan.ca/fr/services/gestion-des-donnees-de-recherche/apprentissage-et-ressources/personnes-ressources-dans-les-etablissements'
- else
- # Handling :'en-CA' locale
- 'https://alliancecan.ca/en/services/research-data-management/learning-and-training/institutional-contacts'
- end
- end
-
def training_resources_path
if I18n.locale == :'fr-CA'
'https://alliancecan.ca/fr/services/gestion-des-donnees-de-recherche/apprentissage-et-ressources/ressources-de-formation'
diff --git a/app/javascript/application.js b/app/javascript/application.js
index 1345f66a4d..8c22536928 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -24,6 +24,7 @@ import 'bootstrap-select';
// Utilities
import './src/utils/accordion';
import './src/utils/autoComplete';
+import './src/utils/copyToken.js';
import './src/utils/externalLink';
import './src/utils/modalSearch';
import './src/utils/outOfFocus';
diff --git a/app/javascript/src/utils/copyToken.js b/app/javascript/src/utils/copyToken.js
new file mode 100644
index 0000000000..3da9c90fb7
--- /dev/null
+++ b/app/javascript/src/utils/copyToken.js
@@ -0,0 +1,35 @@
+const initCopyToken = () => {
+ document.addEventListener('click', function (e) {
+ const button = e.target.closest('#copy-token-btn');
+ if (!button) return;
+
+ e.preventDefault();
+
+ // Prevent spam clicking
+ if (button.disabled) return;
+
+ const tokenInput = document.getElementById('api-token-val');
+ if (!tokenInput) return;
+
+ const originalHTML = button.innerHTML;
+
+ // Disable immediately
+ button.disabled = true;
+
+ navigator.clipboard.writeText(tokenInput.value).then(() => {
+ // Replace button contents with check icon
+ button.innerHTML = '';
+
+ // Restore after 2s
+ setTimeout(() => {
+ button.innerHTML = originalHTML;
+ button.disabled = false;
+ }, 2000);
+ }).catch(() => {
+ button.disabled = false;
+ alert('Failed to copy token');
+ });
+ });
+};
+
+initCopyToken();
diff --git a/app/models/plan.rb b/app/models/plan.rb
index 772e5c7ed5..51cdb3ae01 100644
--- a/app/models/plan.rb
+++ b/app/models/plan.rb
@@ -207,6 +207,39 @@ class Plan < ApplicationRecord
includes(:phases, :sections, :questions, template: [:org]).find(id)
}
+ # Eager loads all associations needed for API v2 serialization,
+ # and restricts to plans where the user_id has an active role.
+ scope :for_api_v2, lambda { |user_id|
+ joins(:roles)
+ .includes(
+ :research_outputs,
+ :template,
+ { identifiers: :identifier_scheme },
+ funder: { identifiers: :identifier_scheme },
+ contributors: [
+ { identifiers: :identifier_scheme },
+ { org: { identifiers: :identifier_scheme } }
+ ],
+ roles: [
+ user: [
+ :language,
+ { identifiers: :identifier_scheme },
+ { org: { identifiers: :identifier_scheme } }
+ ]
+ ],
+ # plan.org is only executed when `plan.funder.present? || plan.grant_id.present? == true`
+ # - (see `app/views/api/v2/plans/_project.json.jbuilder`)
+ # Thus, the following line avoids N+1 queries in some cases,
+ # but performs unnecessary eager loading in others
+ org: [
+ :region,
+ { identifiers: :identifier_scheme }
+ ]
+ )
+ .where(roles: { user_id: user_id, active: true })
+ .distinct
+ }
+
##
# Settings for the template
has_settings :export, class_name: 'Settings::Template' do |s|
diff --git a/app/models/template.rb b/app/models/template.rb
index 3dd54abb02..d32416d453 100644
--- a/app/models/template.rb
+++ b/app/models/template.rb
@@ -215,6 +215,16 @@ class Template < ApplicationRecord
term: "%#{term}%")
}
+ scope :for_api_v2, lambda { |org_id|
+ org_templates = organisationally_visible.where(org_id: org_id)
+ public_templates = publicly_visible.where(customization_of: nil)
+ includes(org: { identifiers: :identifier_scheme })
+ .joins(:org)
+ .published
+ .merge(org_templates.or(public_templates))
+ .order(:title)
+ }
+
# defines the export setting for a template object
has_settings :export, class_name: 'Settings::Template' do |s|
s.key :export, defaults: Settings::Template::DEFAULT_SETTINGS
diff --git a/app/models/user.rb b/app/models/user.rb
index fe9579eb79..fe149e75c9 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -113,6 +113,12 @@ class User < ApplicationRecord
has_and_belongs_to_many :notifications, dependent: :destroy,
join_table: 'notification_acknowledgements'
+ has_many :access_grants, class_name: 'Doorkeeper::AccessGrant', foreign_key: :resource_owner_id,
+ dependent: :delete_all
+
+ has_many :access_tokens, class_name: 'Doorkeeper::AccessToken', foreign_key: :resource_owner_id,
+ dependent: :delete_all
+
# ===============
# = Validations =
# ===============
diff --git a/app/policies/api/v2/plans_policy.rb b/app/policies/api/v2/plans_policy.rb
new file mode 100644
index 0000000000..16fc07edd4
--- /dev/null
+++ b/app/policies/api/v2/plans_policy.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ # Security rules for API V2 Plan endpoints
+ class PlansPolicy < ApplicationPolicy
+ # overriding the initializer due to resource owner / user
+ # not needing to be logged in for client app to make requests
+ def initialize(resource_owner, plan = nil) # rubocop:todo Lint/MissingSuper
+ @resource_owner = resource_owner
+ @plan = plan
+ end
+
+ def show?
+ # The show action uses the resolve method, so only a presence check
+ # is needed here (see the resolve method comment for more).
+ @plan.present?
+ end
+
+ class Scope < Scope # rubocop:todo Style/Documentation
+ def initialize(resource_owner) # rubocop:todo Lint/MissingSuper
+ @resource_owner = resource_owner
+ end
+
+ # Eager loads all associations needed for API v2 serialization,
+ # and restricts to plans where the user_id has an active role.
+ # - (i.e. .where(roles: { user_id: @resource_owner.id, active: true }))
+ def resolve
+ Plan.for_api_v2(@resource_owner.id)
+ end
+ end
+ end
+ end
+end
diff --git a/app/policies/api/v2/templates_policy.rb b/app/policies/api/v2/templates_policy.rb
new file mode 100644
index 0000000000..c12a14737b
--- /dev/null
+++ b/app/policies/api/v2/templates_policy.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ class TemplatesPolicy < ApplicationPolicy
+ class Scope < Scope # rubocop:todo Style/Documentation
+ def initialize(resource_owner) # rubocop:todo Lint/MissingSuper
+ @resource_owner = resource_owner
+ end
+
+ def resolve
+ # get the templates
+ Template.for_api_v2(@resource_owner.org&.id)
+ end
+ end
+ end
+ end
+end
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 978c40d412..dd4527d941 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -55,6 +55,12 @@ def refresh_token?
(@user.can_org_admin? && @user.can_use_api?)
end
+ # Safe: only allows the signed-in user to generate/rotate their own token.
+ # These are first-party, user-scoped tokens and do not affect other users.
+ def internal_user_v2_access_token?
+ true
+ end
+
def merge?
@user.can_super_admin?
end
diff --git a/app/presenters/api/v2/api_presenter.rb b/app/presenters/api/v2/api_presenter.rb
new file mode 100644
index 0000000000..1ad7290ab6
--- /dev/null
+++ b/app/presenters/api/v2/api_presenter.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ # Generic helper methods for API V2
+ class ApiPresenter
+ class << self
+ def boolean_to_yes_no_unknown(value:)
+ return 'unknown' unless value.present?
+
+ value ? 'yes' : 'no'
+ end
+ end
+ end
+ end
+end
diff --git a/app/presenters/api/v2/contributor_presenter.rb b/app/presenters/api/v2/contributor_presenter.rb
new file mode 100644
index 0000000000..77232c2838
--- /dev/null
+++ b/app/presenters/api/v2/contributor_presenter.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ # Helper class for the API V2 contributors views
+ class ContributorPresenter
+ class << self
+ # Convert the specified role into a CRediT Taxonomy URL
+ def role_as_uri(role:)
+ return nil unless role.present?
+ return 'other' if role.to_s.casecmp('other').zero?
+
+ "#{Contributor::ONTOLOGY_BASE_URL}/#{role.to_s.downcase.tr('_', '-')}"
+ end
+
+ def contributor_id(identifiers:)
+ identifiers.find { |id| id.identifier_scheme.name == 'orcid' }
+ end
+ end
+ end
+ end
+end
diff --git a/app/presenters/api/v2/funding_presenter.rb b/app/presenters/api/v2/funding_presenter.rb
new file mode 100644
index 0000000000..c878daadb6
--- /dev/null
+++ b/app/presenters/api/v2/funding_presenter.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ # Helper class for the API V2 funding section
+ class FundingPresenter
+ class << self
+ # If the plan has a grant number then it has been awarded/granted
+ # otherwise it is 'planned'
+ def status(plan:)
+ return 'planned' unless plan.present?
+
+ case plan.funding_status
+ when 'funded'
+ 'granted'
+ when 'denied'
+ 'rejected'
+ else
+ 'planned'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/presenters/api/v2/language_presenter.rb b/app/presenters/api/v2/language_presenter.rb
new file mode 100644
index 0000000000..2be21c7e38
--- /dev/null
+++ b/app/presenters/api/v2/language_presenter.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ # Helper class for the API V2 language values
+ class LanguagePresenter
+ class << self
+ LANGUAGE_MAP = {
+ aa: 'aar', ab: 'abk', af: 'afr', ak: 'aka', am: 'amh', ar: 'ara', an: 'arg',
+ as: 'asm', av: 'ava', ae: 'ave', ay: 'aym', az: 'aze',
+
+ ba: 'bak', bm: 'bam', be: 'bel', bn: 'ben', bh: 'bih', bi: 'bis', bo: 'tib',
+ bs: 'bos', br: 'bre', bg: 'bul',
+
+ ca: 'cat', cs: 'cze', ch: 'cha', ce: 'che', cu: 'chu', cv: 'chv', co: 'cos',
+ cr: 'cre', cy: 'wel',
+
+ da: 'dan', de: 'deu', dv: 'div', dz: 'dzo',
+
+ el: 'gre', en: 'eng', eo: 'epo', es: 'spa', et: 'est', eu: 'baq', ee: 'ewe',
+
+ fo: 'fao', fa: 'per', fj: 'fij', fi: 'fin', fr: 'fre', fy: 'fry', ff: 'ful',
+
+ gd: 'gla', ga: 'gle', gl: 'glg', gv: 'glv', gn: 'grn', gu: 'guj',
+
+ ht: 'hat', ha: 'hau', he: 'heb', hz: 'her', hi: 'hin', ho: 'hmo', hr: 'hrv',
+ hu: 'hun', hy: 'arm',
+
+ ig: 'ibo', io: 'ido', ii: 'iii', iu: 'iku', ie: 'ile', ia: 'ina', id: 'ind',
+ ik: 'ipk', is: 'ice', it: 'ita',
+
+ jv: 'jav', ja: 'jpn',
+
+ kl: 'kal', kn: 'kan', ks: 'kas', kr: 'kau', kk: 'kaz', km: 'khm', ki: 'kik',
+ ky: 'kir', kv: 'kom', kg: 'kon', ko: 'kor', kj: 'kua', ku: 'kur', ka: 'geo',
+ kw: 'cor',
+
+ lo: 'lao', la: 'lat', lv: 'lav', li: 'lim', ln: 'lin', lt: 'lit', lb: 'ltz',
+ lu: 'lub', lg: 'lug',
+
+ mk: 'mac', mh: 'mah', ml: 'mal', mi: 'mao', mr: 'mar', ms: 'may', mg: 'mlg',
+ mt: 'mlt', mn: 'mon', my: 'bur',
+
+ na: 'nau', nv: 'nav', nr: 'nbl', nd: 'nde', ng: 'ndo', ne: 'nep', nl: 'dut',
+ nn: 'nno', nb: 'nob', no: 'nor', ny: 'nya',
+
+ oc: 'oci', oj: 'oji', or: 'ori', om: 'orm', os: 'oss',
+
+ pa: 'pan', pi: 'pli', pl: 'pol', pt: 'por', ps: 'pus',
+
+ qu: 'que',
+
+ rm: 'roh', ro: 'rum', rn: 'run', ru: 'rus', rw: 'kin',
+
+ sg: 'sag', sa: 'san', si: 'sin', sk: 'slo', sl: 'slv', se: 'sme', sm: 'smo',
+ sn: 'sna', sd: 'snd', so: 'som', st: 'sot', sq: 'alb', sc: 'srd', sr: 'srp',
+ ss: 'ssw', su: 'sun', sw: 'swa', sv: 'swe',
+
+ ty: 'tah', ta: 'tam', tt: 'tat', te: 'tel', tg: 'tgk', tl: 'tgl', th: 'tha',
+ ti: 'tir', to: 'ton', tn: 'tsn', ts: 'tso', tk: 'tuk', tr: 'tur', tw: 'twi',
+
+ ug: 'uig', uk: 'ukr', ur: 'urd', uz: 'uzb',
+
+ ve: 'ven', vi: 'vie', vo: 'vol',
+
+ wa: 'wln', wo: 'wol',
+
+ xh: 'xho',
+
+ yi: 'yid', yo: 'yor',
+
+ za: 'zha', zh: 'chi', zu: 'zul'
+ }.freeze
+
+ # Convert the incoming 2 (e.g. en - ISO 639-1) or 2+region (e.g. en-UK)
+ # into the 3 character code (e.g. eng - ISO 639-2)
+ def three_char_code(lang:)
+ lang ||= LocaleService.default_locale
+ two_char_code = lang.to_s.split('-').first
+ LANGUAGE_MAP[two_char_code.to_sym]
+ end
+ end
+ end
+ end
+end
diff --git a/app/presenters/api/v2/org_presenter.rb b/app/presenters/api/v2/org_presenter.rb
new file mode 100644
index 0000000000..5302acb7a7
--- /dev/null
+++ b/app/presenters/api/v2/org_presenter.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ # Helper class for the API V2 affiliation sections
+ class OrgPresenter
+ class << self
+ def affiliation_id(identifiers:)
+ ident = identifiers.find { |id| id.identifier_scheme&.name == 'ror' }
+ return ident if ident.present?
+
+ identifiers.find { |id| id.identifier_scheme&.name == 'fundref' }
+ end
+ end
+ end
+ end
+end
diff --git a/app/presenters/api/v2/pagination_presenter.rb b/app/presenters/api/v2/pagination_presenter.rb
new file mode 100644
index 0000000000..1b8fbc1109
--- /dev/null
+++ b/app/presenters/api/v2/pagination_presenter.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ # Helper class for genewric API V2 pagination
+ class PaginationPresenter
+ def initialize(current_url:, per_page:, total_items:, current_page: 1)
+ @url = current_url
+ @per_page = per_page
+ @total_items = total_items
+ @page = current_page
+ end
+
+ def url_without_pagination
+ return nil unless @url.present? && @url.is_a?(String)
+
+ url = @url.gsub(/per_page=\d+/, '')
+ .gsub(/page=\d+/, '')
+ .gsub(/(&)+$/, '').gsub(/\?$/, '')
+
+ (url.include?('?') ? "#{url}&" : "#{url}?")
+ end
+
+ def prev_page?
+ total_pages > 1 && @page != 1
+ end
+
+ def next_page?
+ total_pages > 1 && @page < total_pages
+ end
+
+ def prev_page_link
+ "#{url_without_pagination}page=#{@page - 1}&per_page=#{@per_page}"
+ end
+
+ def next_page_link
+ "#{url_without_pagination}page=#{@page + 1}&per_page=#{@per_page}"
+ end
+
+ private
+
+ def total_pages
+ return 1 unless @total_items.present? && @per_page.present? &&
+ @total_items.positive? && @per_page.positive?
+
+ (@total_items.to_f / @per_page).ceil
+ end
+ end
+ end
+end
diff --git a/app/presenters/api/v2/plan_presenter.rb b/app/presenters/api/v2/plan_presenter.rb
new file mode 100644
index 0000000000..8cb7eca6ef
--- /dev/null
+++ b/app/presenters/api/v2/plan_presenter.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ # Helper class for the API V2 project / DMP
+ class PlanPresenter
+ attr_reader :data_contact, :contributors, :costs, :complete_plan_data
+
+ def initialize(plan:, complete: false)
+ @contributors = []
+ return unless plan.present?
+
+ @plan = plan
+
+ # Use owner or first data_curation role as the data_contact
+ @data_contact = @plan.owner || @plan.contributors.find(&:data_curation?)
+ @contributors = @plan.contributors.to_a
+
+ @costs = plan_costs(plan: @plan)
+
+ @complete_plan_data = fetch_all_q_and_a if complete
+ end
+
+ # Extract the ARK or DOI for the DMP OR use its URL if none exists
+ def identifier
+ doi = @plan.identifiers.select do |id|
+ ::Plan::DMP_ID_TYPES.include?(id.identifier_format)
+ end
+ return doi.first if doi.first.present?
+
+ # if no DOI then use the URL for the API's 'show' method
+ Identifier.new(value: Rails.application.routes.url_helpers.api_v2_plan_url(@plan))
+ end
+
+ private
+
+ # Retrieve the answers that have the Budget theme
+ def plan_costs(plan:)
+ theme = Theme.where(title: 'Cost').first
+ return [] unless theme.present?
+
+ # TODO: define a new 'Currency' question type that includes a float field
+ # any currency type selector (e.g GBP or USD)
+ answers = plan.answers
+ .joins(question: :themes)
+ .where(themes: { id: theme.id })
+ .includes(:question)
+
+ answers.map do |answer|
+ # TODO: Investigate whether question level guidance should be the description
+ { title: answer.question.text, description: nil,
+ currency_code: 'usd', value: answer.text }
+ end
+ end
+
+ # Fetch all questions and answers from a plan, regardless of theme
+ def fetch_all_q_and_a
+ answers = @plan.answers
+ return [] unless answers.present?
+
+ answers.filter_map do |answer|
+ q = answer.question
+ next unless q.present?
+
+ {
+ id: q.id,
+ title: "Question #{q.number || q.id}",
+ section: q.section&.title,
+ question: q.text.to_s,
+ answer: answer.text.to_s
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/presenters/api/v2/research_output_presenter.rb b/app/presenters/api/v2/research_output_presenter.rb
new file mode 100644
index 0000000000..9571259e82
--- /dev/null
+++ b/app/presenters/api/v2/research_output_presenter.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ # Helper methods for research outputs
+ class ResearchOutputPresenter
+ include ActionView::Helpers::SanitizeHelper
+ attr_reader :dataset_id, :preservation_statement, :security_and_privacy, :license_start_date,
+ :data_quality_assurance, :distributions, :metadata, :technical_resources
+
+ def initialize(output:)
+ @research_output = output
+ return unless output.is_a?(ResearchOutput)
+
+ @plan = output.plan
+ @dataset_id = identifier
+
+ load_narrative_content
+
+ @license_start_date = determine_license_start_date(output: output)
+ end
+
+ private
+
+ def identifier
+ Identifier.new(identifiable: @research_output, value: @research_output.id)
+ end
+
+ def determine_license_start_date(output:)
+ return nil unless output.present?
+ return output.release_date.to_formatted_s(:iso8601) if output.release_date.present?
+
+ output.created_at.to_formatted_s(:iso8601)
+ end
+
+ def load_narrative_content
+ @preservation_statement = ''
+ @security_and_privacy = []
+ @data_quality_assurance = ''
+
+ # Disabling rubocop here since a guard clause would make the line too long
+ # rubocop:disable Style/GuardClause
+ if Rails.configuration.x.madmp.extract_preservation_statements_from_themed_questions
+ @preservation_statement = fetch_q_and_a_as_single_statement(themes: %w[Preservation])
+ end
+ if Rails.configuration.x.madmp.extract_security_privacy_statements_from_themed_questions
+ @security_and_privacy = fetch_q_and_a(themes: ['Ethics & privacy', 'Storage & security'])
+ end
+ if Rails.configuration.x.madmp.extract_data_quality_statements_from_themed_questions
+ @data_quality_assurance = fetch_q_and_a_as_single_statement(themes: ['Data Collection'])
+ end
+ # rubocop:enable Style/GuardClause
+ end
+
+ def fetch_q_and_a_as_single_statement(themes:)
+ fetch_q_and_a(themes: themes).collect { |item| item[:description] }.join('
')
+ end
+
+ def fetch_q_and_a(themes:)
+ return [] unless themes.is_a?(Array) && themes.any?
+
+ answers = answers_for_themes(themes)
+
+ descs_by_theme = build_descriptions_by_theme_hash(answers, themes)
+
+ descs_by_theme.map do |theme, descs|
+ { title: theme, description: descs }
+ end
+ end
+
+ def answers_for_themes(themes)
+ @plan.answers
+ .joins(question: :themes)
+ .where(themes: { title: themes })
+ .includes(question: :themes)
+ .distinct
+ end
+
+ def build_descriptions_by_theme_hash(answers, themes)
+ descs_by_theme = Hash.new { |h, k| h[k] = [] }
+
+ answers.each do |answer|
+ answer.question.themes.each do |theme|
+ next unless themes.include?(theme.title)
+
+ descs_by_theme[theme.title] << format_q_and_a(answer.question, answer)
+ end
+ end
+ descs_by_theme
+ end
+
+ def format_q_and_a(question, answer)
+ "Question: #{sanitize(question.text)}
" \
+ "Answer: #{sanitize(answer.text)}"
+ end
+ end
+ end
+end
diff --git a/app/presenters/api/v2/template_presenter.rb b/app/presenters/api/v2/template_presenter.rb
new file mode 100644
index 0000000000..2d32a45d37
--- /dev/null
+++ b/app/presenters/api/v2/template_presenter.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ # Helper class for the API V2 template info
+ class TemplatePresenter
+ def initialize(template:)
+ @template = template
+ end
+
+ # If the plan has a grant number then it has been awarded/granted
+ # otherwise it is 'planned'
+ def title
+ return @template.title unless @template.customization_of.present?
+
+ "#{@template.title} - with additional questions for #{@template.org.name}"
+ end
+ end
+ end
+end
diff --git a/app/services/api/v2/conversion_service.rb b/app/services/api/v2/conversion_service.rb
new file mode 100644
index 0000000000..4a9a932878
--- /dev/null
+++ b/app/services/api/v2/conversion_service.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ # Helper service that translates to/from the RDA common standard
+ class ConversionService
+ class << self
+ # Converts a boolean field to [yes, no, unknown]
+ def boolean_to_yes_no_unknown(value)
+ return 'yes' if [true, 1].include?(value)
+
+ return 'no' if [false, 0].include?(value)
+
+ 'unknown'
+ end
+
+ # Converts a [yes, no, unknown] field to boolean (or nil)
+ def yes_no_unknown_to_boolean(value)
+ return true if value&.downcase == 'yes'
+
+ return nil if value.blank? || value&.downcase == 'unknown'
+
+ false
+ end
+
+ # Converts the context and value into an Identifier with a psuedo
+ # IdentifierScheme for display in JSON partials. Which will result in:
+ # { type: 'context', identifier: 'value' }
+ def to_identifier(context:, value:)
+ return nil unless value.present? && context.present?
+
+ scheme = IdentifierScheme.new(name: context)
+ Identifier.new(value: value, identifier_scheme: scheme)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/api/v2/internal_user_access_token_service.rb b/app/services/api/v2/internal_user_access_token_service.rb
new file mode 100644
index 0000000000..7eeac14f5b
--- /dev/null
+++ b/app/services/api/v2/internal_user_access_token_service.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Api
+ module V2
+ # Service responsible for user-scoped v2 API access tokens, strictly for
+ # internal users of this application.
+ #
+ # Tokens issued by this service are functionally equivalent to Personal Access
+ # Tokens (PATs) for first-party usage. They are minted directly for a user
+ # who is already authenticated in the application, bypassing the standard
+ # OAuth 2.0 authorization_code redirect and consent flow.
+ #
+ # This design is intentional:
+ # - tokens are internal to this application (first-party)
+ # - tokens are owned by a single user and scoped accordingly
+ # - token creation, rotation, and revocation happen entirely within the app UI
+ #
+ # Tokens are stored as Doorkeeper::AccessToken records to leverage existing
+ # scoping, expiry, and revocation mechanisms.
+ #
+ # This service does NOT support third-party OAuth clients or delegated consent flows.
+ class InternalUserAccessTokenService
+ READ_SCOPE = 'read'
+ INTERNAL_OAUTH_APP_NAME = Rails.application.config.x.application.internal_oauth_app_name
+
+ class << self
+ def rotate!(user)
+ revoke_existing!(user)
+
+ token = Doorkeeper::AccessToken.create!(
+ application_id: application!.id,
+ resource_owner_id: user.id,
+ scopes: READ_SCOPE,
+ expires_in: 24.hours # Overrides Doorkeeper's `access_token_expires_in`
+ )
+ token.plaintext_token
+ end
+
+ # Used by views (e.g. devise/registrations/_v2_api_token.html.erb) to safely
+ # gate token UI if the internal OAuth application is missing.
+ def application_present?
+ application!
+ true
+ rescue StandardError => e
+ Rails.logger.error(e.message)
+ false
+ end
+
+ private
+
+ def application!
+ Doorkeeper::Application.find_by(name: INTERNAL_OAUTH_APP_NAME) ||
+ raise(
+ StandardError,
+ "Required Doorkeeper application '#{INTERNAL_OAUTH_APP_NAME}' not found. " \
+ 'Please ensure the application exists in the database.'
+ )
+ end
+
+ def revoke_existing!(user)
+ Doorkeeper::AccessToken.revoke_all_for(application!.id, user)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/external_apis/ror_service.rb b/app/services/external_apis/ror_service.rb
index 3c646105c9..82e88123ae 100644
--- a/app/services/external_apis/ror_service.rb
+++ b/app/services/external_apis/ror_service.rb
@@ -77,7 +77,7 @@ def search(term:, filters: [])
private
- # Queries the ROR API for the sepcified name and page
+ # Queries the ROR API for the specified name and page
def query_ror(term:, page: 1, filters: [])
return [] unless term.present?
@@ -142,69 +142,126 @@ def parse_results(json:)
return results unless json.present? && json.fetch('items', []).any?
json['items'].each do |item|
- next unless item['id'].present? && item['name'].present?
-
results << {
ror: item['id'].gsub(/^#{landing_page_url}/, ''),
name: org_name(item: item),
- sort_name: item['name'],
- url: item.fetch('links', []).first,
+ sort_name: sort_name(item: item),
+ url: org_url(item: item),
language: org_language(item: item),
fundref: fundref_id(item: item),
- abbreviation: item.fetch('acronyms', []).first
+ abbreviation: org_abbreviation(item: item)
}
+ rescue KeyError, NoMethodError => e
+ Rails.logger.error(
+ "Invalid ROR record: #{e.class} - #{e.message}, item: #{item.inspect}"
+ )
end
results
end
# rubocop:enable Metrics/AbcSize
+ def ror_display_entry(item:)
+ item['names'].find { |n| n['types'].include?('ror_display') }
+ end
+
+ # Extracts the org's display name from `names` to be used as sort_name
+ # "names": [
+ # {"lang": "en", "types": ["ror_display","label"], "value": "Harvard University"},
+ # {"lang": "es","types": ["label"], "value": "Universidad de Harvard"}
+ # ]
+ def sort_name(item:)
+ ror_display_entry(item: item).fetch('value')
+ end
+
+ # Returns the website link value
+ # "links": [
+ # { "type": "website", "value": "https://example.edu" },
+ # { "type": "Wikipedia", "value": "https://en.wikipedia.org/wiki/Example_University" }
+ # ]
+ def org_url(item:)
+ item['links']&.find { |l| l['type'] == 'website' }&.fetch('value')
+ end
+
+ # Returns the country name from locations
+ # "locations" : [ { "geonames_details" : { "country_name" : "Germany",}, "geonames_id" : 2928810 } ]
+ def org_country(item:)
+ location = item['locations']&.find { |l| l['geonames_details'].present? }
+ location&.dig('geonames_details', 'country_name').to_s
+ end
+
+ # Extract acronym/abbreviation from names
+ # "names" : [ { "value" : "UC", "types": ["acronym"], "lang" : "en" } ]
+ def org_abbreviation(item:)
+ item['names'].find { |n| n['types'].include?('acronym') }&.fetch('value')
+ end
+
# Org names are not unique, so include the Org URL if available or
# the country. For example:
# "Example College (example.edu)"
# "Example College (Brazil)"
def org_name(item:)
- return '' unless item.present? && item['name'].present?
+ name = sort_name(item: item)
- country = item.fetch('country', {}).fetch('country_name', '')
+ country = org_country(item: item)
website = org_website(item: item)
+
# If no website or country then just return the name
- return item['name'] unless website.present? || country.present?
+ return name unless website.present? || country.present?
# Otherwise return the contextualized name
- "#{item['name']} (#{website || country})"
+ "#{name} (#{website || country})"
end
- # Extracts the org's ISO639 if available
+ # Extracts the org's language
+ # {
+ # "id": "https://ror.org/012345678",
+ # "names": [
+ # { "value": "Université de Montréal", "types": ["ror_display"], "lang": "fr" },
+ # { "value": "University of Montreal", "types": ["alias"], "lang": "en" }
+ # ]
+ # }
def org_language(item:)
- dflt = I18n.default_locale || 'en'
- return dflt unless item.present?
-
- labels = item.fetch('labels', [{ iso639: dflt }])
- labels.first&.fetch('iso639', I18n.default_locale) || dflt
+ ror_display_entry(item: item)['lang'] || I18n.default_locale.to_s
end
# Extracts the website domain from the item
+ # "links": [
+ # { "type": "website", "value": "https://example.edu" },
+ # { "type": "Wikipedia", "value": "https://en.wikipedia.org/wiki/Example_University" }
+ # ]
def org_website(item:)
- return nil unless item.present? && item.fetch('links', [])&.any?
- return nil if item['links'].first.blank?
+ link = org_url(item: item)
+ return nil unless link.present?
# A website was found, so extract just the domain without the www
domain_regex = %r{^(?:http://|www\.|https://)([^/]+)}
- website = item['links'].first.scan(domain_regex).last.first
+ matches = link.scan(domain_regex)
+ return nil if matches.empty?
+
+ website = matches.last.first
website.gsub('www.', '')
end
- # Extracts the FundRef Id if available
+ # Extracts the FundRef Id from external ids if available
+ # "external_ids": [
+ # {"type": "fundref", "preferred": "12345", "all": ["12345", "67890"]},
+ # {"type": "SomeOtherID", "preferred": "501100000000", "all": ["501100000000"]}
+ # ]
def fundref_id(item:)
- return '' unless item.present? && item['external_ids'].present?
- return '' unless item['external_ids'].fetch('FundRef', {}).any?
+ external_ids = item['external_ids']
+
+ fundref = external_ids.find { |id| id['type'] == 'fundref' }
+ return '' unless fundref.present?
# If a preferred Id was specified then use it
- ret = item['external_ids'].fetch('FundRef', {}).fetch('preferred', '')
- return ret if ret.present?
+ preferred = fundref['preferred']
+ return preferred if preferred.present?
# Otherwise take the first one listed
- item['external_ids'].fetch('FundRef', {}).fetch('all', []).first
+ all = fundref['all']
+ return all.first if all.present?
+
+ ''
end
end
end
diff --git a/app/services/org_selection/hash_to_org_service.rb b/app/services/org_selection/hash_to_org_service.rb
index 27b2e6ef5a..a301d5faaa 100644
--- a/app/services/org_selection/hash_to_org_service.rb
+++ b/app/services/org_selection/hash_to_org_service.rb
@@ -123,9 +123,19 @@ def abbreviation_from_hash(hash:)
# Get the language from the hash or use the default
def language_from_hash(hash:)
- return Language.default unless hash.present? && hash[:language].present?
+ abbr = hash&.dig(:language)
+ # RorService.org_language returns I18n.default_locale.to_s as a fallback
+ return Language.default if abbr.blank? || abbr == I18n.default_locale.to_s
- Language.where(abbreviation: hash[:language]).first || Language.default
+ # ROR provides ISO 639-1 codes (e.g., "en"). Attempt to match against BCP 47 tags (e.g., "en-CA") in db
+ pattern = "#{ActiveRecord::Base.sanitize_sql_like(abbr)}-%"
+
+ results = Language.where('abbreviation = ? OR abbreviation LIKE ?', abbr, pattern)
+ .order(default_language: :desc) # prefer default language if multiple results exist
+ .to_a
+
+ # Return (in order): exact match || first pattern match || app default language
+ results.find { |lang| lang.abbreviation == abbr } || results.first || Language.default
end
def identifier_keys
diff --git a/app/services/orgs/cleanup_unmanaged_orgs_with_users_service.rb b/app/services/orgs/cleanup_unmanaged_orgs_with_users_service.rb
new file mode 100644
index 0000000000..e8130d9c83
--- /dev/null
+++ b/app/services/orgs/cleanup_unmanaged_orgs_with_users_service.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+module Orgs
+ # Invoked by the `orgs:cleanup_unmanaged_orgs_with_users` Rake task.
+ # Cleanup service for unmanaged orgs having 1 or more users.
+ # For each such org:
+ # - Reassign associated users and plans to the "default org"
+ # - Attempt deletion of the org
+ module CleanupUnmanagedOrgsWithUsersService
+ extend self
+
+ Result = Struct.new(
+ :processed_orgs_count,
+ :reassigned_users_count,
+ :reassigned_plans_count,
+ :deleted_orgs_count,
+ :failed_deletions_count,
+ keyword_init: true
+ )
+
+ def run
+ result = initial_result
+
+ default_org = fetch_default_org
+
+ unmanaged_orgs_with_users.find_each do |org|
+ result.processed_orgs_count += 1
+ Org.transaction do
+ reassign_users_to_default_org(org, default_org, result)
+ reassign_plans_to_default_org(org, default_org, result)
+ end
+ # Don't rollback org.users + org.plans reassignment if the org cannot be deleted
+ handle_org_deletion(org, result)
+ end
+
+ print_summary(result)
+ end
+
+ private
+
+ def initial_result
+ Result.new(
+ processed_orgs_count: 0,
+ reassigned_users_count: 0,
+ reassigned_plans_count: 0,
+ deleted_orgs_count: 0,
+ failed_deletions_count: 0
+ )
+ end
+
+ def fetch_default_org
+ default_org = Org.find(Rails.application.config.default_funder_id)
+ puts '----------------------------------------------------------------------------------------------'
+ puts "Using Org with id: '#{default_org.id}', name: '#{default_org.name}' for users/plans reassignment."
+ default_org
+ end
+
+ def unmanaged_orgs_with_users
+ # Fetch unmanaged orgs that have one or more users
+ orgs = Org.unmanaged.joins(:users).distinct
+ puts "Found #{orgs.count} unmanaged org(s) with users to process."
+ orgs
+ end
+
+ def reassign_users_to_default_org(org, default_org, result)
+ reassigned_count = org.users.update_all(org_id: default_org.id)
+ result.reassigned_users_count += reassigned_count
+ puts "✅ Reassigned #{reassigned_count} user(s) from '#{org.name}'."
+ end
+
+ def reassign_plans_to_default_org(org, default_org, result)
+ reassigned_count = org.plans.update_all(org_id: default_org.id)
+ result.reassigned_plans_count += reassigned_count
+ puts "✅ Reassigned #{reassigned_count} plan(s) from '#{org.name}'."
+ end
+
+ def handle_org_deletion(org, result)
+ if org.destroy
+ increment_successful_org_deletions(org, result)
+ else
+ msg = org.errors.full_messages.presence || ["Unknown deletion error (org inspect: #{org.inspect})"]
+ increment_failed_org_deletions(org, result, msg)
+ end
+ rescue StandardError => e
+ increment_failed_org_deletions(org, result, ["Exception: #{e.message}"])
+ end
+
+ def increment_successful_org_deletions(org, result)
+ result.deleted_orgs_count += 1
+ puts "✅ Deleted unmanaged org: #{org.id} - #{org.name}"
+ end
+
+ def increment_failed_org_deletions(org, result, messages)
+ result.failed_deletions_count += 1
+ puts "⚠️ Failed to delete unmanaged org: #{org.id} - #{org.name}: #{messages.join(', ')}"
+ end
+
+ def print_summary(result)
+ puts <<~MSG
+ ----- Summary -----
+ Unmanaged orgs processed: #{result.processed_orgs_count}
+ Users reassigned: #{result.reassigned_users_count}
+ Plans reassigned: #{result.reassigned_plans_count}
+ Successfully deleted orgs: #{result.deleted_orgs_count}
+ Failed org deletions: #{result.failed_deletions_count}
+ MSG
+ end
+ end
+end
diff --git a/app/views/api/v2/_standard_response.json.jbuilder b/app/views/api/v2/_standard_response.json.jbuilder
new file mode 100644
index 0000000000..372c42cb31
--- /dev/null
+++ b/app/views/api/v2/_standard_response.json.jbuilder
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+# locals: response, request, total_items
+
+total_items ||= 0
+
+paginator = Api::V2::PaginationPresenter.new(current_url: request.path,
+ per_page: @per_page,
+ total_items: total_items,
+ current_page: @page)
+
+json.prettify!
+json.ignore_nil!
+
+json.application @application
+json.source "#{request.method} #{request.path}"
+json.time Time.now.to_formatted_s(:iso8601)
+json.caller @caller
+json.code response.status
+json.message Rack::Utils::HTTP_STATUS_CODES[response.status]
+
+if response.status == 200
+
+ # Pagination Links
+ if total_items.positive?
+ json.page @page
+ json.per_page @per_page
+ json.total_items total_items
+
+ # Prepare the base URL by removing the old pagination params
+ json.prev paginator.prev_page_link if paginator.prev_page?
+ json.next paginator.next_page_link if paginator.next_page?
+ else
+ json.total_items 0
+ end
+
+end
diff --git a/app/views/api/v2/contributors/_show.json.jbuilder b/app/views/api/v2/contributors/_show.json.jbuilder
new file mode 100644
index 0000000000..80e6d27f3a
--- /dev/null
+++ b/app/views/api/v2/contributors/_show.json.jbuilder
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+# locals: contributor, is_contact
+
+is_contact ||= false
+
+name = contributor.is_a?(User) ? contributor.name(false) : contributor.name
+json.name sanitize(name)
+json.mbox contributor.email
+
+if !is_contact && contributor.selected_roles.any?
+ roles = contributor.selected_roles.map do |role|
+ Api::V2::ContributorPresenter.role_as_uri(role: role)
+ end
+ json.role roles if roles.any?
+end
+
+if contributor.org.present?
+ json.affiliation do
+ json.partial! 'api/v2/orgs/show', org: contributor.org
+ end
+end
+
+orcid = contributor.identifier_for_scheme(scheme: 'orcid')
+if orcid.present?
+ id = Api::V2::ContributorPresenter.contributor_id(
+ identifiers: contributor.identifiers
+ )
+ if is_contact
+ json.contact_id do
+ json.partial! 'api/v2/identifiers/show', identifier: id
+ end
+ else
+ json.contributor_id do
+ json.partial! 'api/v2/identifiers/show', identifier: id
+ end
+ end
+end
diff --git a/app/views/api/v2/datasets/_show.json.jbuilder b/app/views/api/v2/datasets/_show.json.jbuilder
new file mode 100644
index 0000000000..301a7d0f9d
--- /dev/null
+++ b/app/views/api/v2/datasets/_show.json.jbuilder
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+# locals: output
+
+if output.is_a?(ResearchOutput)
+ presenter = Api::V2::ResearchOutputPresenter.new(output: output)
+
+ json.type output.output_type
+ json.title strip_tags(output.title)
+ json.description sanitize(output.description)
+ json.personal_data Api::V2::ApiPresenter.boolean_to_yes_no_unknown(value: output.personal_data)
+ json.sensitive_data Api::V2::ApiPresenter.boolean_to_yes_no_unknown(value: output.sensitive_data)
+ json.issued output.release_date&.to_formatted_s(:iso8601)
+
+ json.preservation_statement presenter.preservation_statement
+ json.security_and_privacy presenter.security_and_privacy
+ json.data_quality_assurance presenter.data_quality_assurance
+
+ json.dataset_id do
+ json.partial! "api/v2/identifiers/show", identifier: presenter.dataset_id
+ end
+
+ json.distribution output.repositories do |repository|
+ json.title "Anticipated distribution for #{output.title}"
+ json.byte_size output.byte_size
+ json.data_access output.access
+
+ json.host do
+ json.title repository.name
+ json.description repository.description
+ json.url repository.homepage
+
+ # DMPTool extensions to the RDA common metadata standard
+ json.dmproadmap_host_id do
+ json.type "url"
+ json.identifier repository.uri
+ end
+ end
+
+ if output.license.present?
+ json.license [output.license] do |license|
+ json.license_ref license.uri
+ json.start_date presenter.license_start_date
+ end
+ end
+ end
+
+ json.metadata output.metadata_standards do |metadata_standard|
+ website = metadata_standard.locations.find { |loc| loc["type"] == "website" }
+ website = { url: "" } unless website.present?
+
+ descr_array = [metadata_standard.title, metadata_standard.description, website["url"]]
+ json.description descr_array.join(" - ")
+
+ json.metadata_standard_id do
+ json.type "url"
+ json.identifier metadata_standard.uri
+ end
+ end
+
+ json.technical_resource []
+
+ if output.plan.research_domain_id.present?
+ research_domain = ResearchDomain.find_by(id: output.plan.research_domain_id)
+ if research_domain.present?
+ combined = "#{research_domain.identifier} - #{research_domain.label}"
+ json.keyword [research_domain.label, combined]
+ end
+ end
+
+else
+ json.type "dataset"
+ json.title "Generic dataset"
+ json.description "No individual datasets have been defined for this DMP."
+
+ if output.research_domain_id.present?
+ research_domain = ResearchDomain.find_by(id: output.research_domain_id)
+ if research_domain.present?
+ combined = "#{research_domain.identifier} - #{research_domain.label}"
+ json.keyword [research_domain.label, combined]
+ end
+ end
+end
diff --git a/app/views/api/v2/error.json.jbuilder b/app/views/api/v2/error.json.jbuilder
new file mode 100644
index 0000000000..ac08f26d9f
--- /dev/null
+++ b/app/views/api/v2/error.json.jbuilder
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+json.partial! 'api/v2/standard_response'
+
+# json.items []
+json.message @payload[:message]
+json.details @payload[:details]
diff --git a/app/views/api/v2/heartbeat.json.jbuilder b/app/views/api/v2/heartbeat.json.jbuilder
new file mode 100644
index 0000000000..70b165b95d
--- /dev/null
+++ b/app/views/api/v2/heartbeat.json.jbuilder
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+json.partial! 'api/v2/standard_response'
+
+json.items []
diff --git a/app/views/api/v2/identifiers/_show.json.jbuilder b/app/views/api/v2/identifiers/_show.json.jbuilder
new file mode 100644
index 0000000000..c219222aee
--- /dev/null
+++ b/app/views/api/v2/identifiers/_show.json.jbuilder
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+# locals: identifier
+
+json.type identifier&.identifier_format
+json.identifier identifier&.value
diff --git a/app/views/api/v2/me.json.jbuilder b/app/views/api/v2/me.json.jbuilder
new file mode 100644
index 0000000000..18a1e7c114
--- /dev/null
+++ b/app/views/api/v2/me.json.jbuilder
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+json.partial! 'api/v2/standard_response'
+
+if current_user.present?
+ json.items [current_user] do |user|
+ json.name [user.surname, user.firstname].join(', ')
+ json.mbox user.email
+ json.token user.ui_token
+
+ if user.org.present? && ['No funder', 'Non Partner Institution'].exclude?(user.org.name)
+ json.affiliation do
+ json.partial! 'api/v2/orgs/show', org: user.org
+ end
+ end
+
+ orcid = user.identifier_for_scheme(scheme: 'orcid')
+ if orcid.present?
+ json.user_id do
+ json.partial! 'api/v2/identifiers/show', identifier: orcid
+ end
+ end
+ end
+
+else
+ json.items []
+end
diff --git a/app/views/api/v2/orgs/_show.json.jbuilder b/app/views/api/v2/orgs/_show.json.jbuilder
new file mode 100644
index 0000000000..934e429a17
--- /dev/null
+++ b/app/views/api/v2/orgs/_show.json.jbuilder
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# locals: org
+
+json.name org.name
+json.abbreviation org.abbreviation
+json.region org.region&.abbreviation
+
+if org.identifiers.any?
+ json.affiliation_id do
+ id = Api::V2::OrgPresenter.affiliation_id(identifiers: org.identifiers)
+ json.partial! 'api/v2/identifiers/show', identifier: id
+ end
+end
diff --git a/app/views/api/v2/plans/_cost.json.jbuilder b/app/views/api/v2/plans/_cost.json.jbuilder
new file mode 100644
index 0000000000..ac0d0a5add
--- /dev/null
+++ b/app/views/api/v2/plans/_cost.json.jbuilder
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+# locals: cost
+
+json.title sanitize(cost[:title])
+json.description cost[:description]
+json.currency_code cost[:currency_code]
+json.value sanitize(cost[:value])
diff --git a/app/views/api/v2/plans/_funding.json.jbuilder b/app/views/api/v2/plans/_funding.json.jbuilder
new file mode 100644
index 0000000000..35786ac2dc
--- /dev/null
+++ b/app/views/api/v2/plans/_funding.json.jbuilder
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+# locals: plan
+
+json.name plan.funder&.name
+
+if plan.funder.present?
+ id = Api::V2::OrgPresenter.affiliation_id(identifiers: plan.funder.identifiers)
+
+ if id.present?
+ json.funder_id do
+ json.partial! 'api/v2/identifiers/show', identifier: id
+ end
+ end
+end
+
+if plan.grant_id.present? && plan.grant.present?
+ json.grant_id do
+ json.partial! 'api/v2/identifiers/show', identifier: plan.grant
+ end
+end
+
+json.funding_status Api::V2::FundingPresenter.status(plan: plan)
+
+# DMPTool extensions to the RDA common metadata standard
+# ------------------------------------------------------
+
+# We collect a user entered ID on the form, so this is a way to convey it to other systems
+# The ID would typically be something relevant to the funder or research organization
+if plan.identifier.present?
+ json.dmproadmap_funding_opportunity_id do
+ json.partial! 'api/v2/identifiers/show', identifier: Identifier.new(identifiable: plan,
+ value: plan.identifier)
+ end
+end
+
+# Since the Plan owner (aka contact) and contributor orgs could be different than the
+# one associated with the Plan, we add it here.
+json.dmproadmap_funded_affiliations [plan.org] do |funded_org|
+ json.partial! 'api/v2/orgs/show', org: funded_org
+end
diff --git a/app/views/api/v2/plans/_project.json.jbuilder b/app/views/api/v2/plans/_project.json.jbuilder
new file mode 100644
index 0000000000..9bf3f97fea
--- /dev/null
+++ b/app/views/api/v2/plans/_project.json.jbuilder
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+# locals: plan
+
+json.title plan.title
+json.description plan.description
+
+start_date = plan.start_date || Time.now
+json.start start_date.to_formatted_s(:iso8601)
+
+end_date = plan.end_date || (Time.now + 2.years)
+json.end end_date&.to_formatted_s(:iso8601)
+
+if plan.funder.present? || plan.grant_id.present?
+ json.funding [plan] do
+ json.partial! 'api/v2/plans/funding', plan: plan
+ end
+end
diff --git a/app/views/api/v2/plans/_show.json.jbuilder b/app/views/api/v2/plans/_show.json.jbuilder
new file mode 100644
index 0000000000..6029f88c0d
--- /dev/null
+++ b/app/views/api/v2/plans/_show.json.jbuilder
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+# locals: plan
+
+json.schema 'https://github.com/RDA-DMP-Common/RDA-DMP-Common-Standard/tree/master/examples/JSON/JSON-schema/1.0'
+
+presenter = Api::V2::PlanPresenter.new(plan: plan, complete: @complete)
+
+# Note the symbol of the dmproadmap json object
+# nested in extensions which is the container for the json template object, etc.
+
+# A JSON representation of a Data Management Plan in the
+# RDA Common Standard format
+json.title plan.title
+json.description plan.description
+json.language Api::V2::LanguagePresenter.three_char_code(
+ lang: plan.owner&.language&.abbreviation
+)
+json.created plan.created_at.to_formatted_s(:iso8601)
+json.modified plan.updated_at.to_formatted_s(:iso8601)
+
+json.ethical_issues_exist Api::V2::ConversionService.boolean_to_yes_no_unknown(plan.ethical_issues)
+json.ethical_issues_description sanitize(plan.ethical_issues_description)
+json.ethical_issues_report plan.ethical_issues_report
+
+id = presenter.identifier
+if id.present?
+ json.dmp_id do
+ json.partial! 'api/v2/identifiers/show', identifier: id
+ end
+end
+
+if presenter.data_contact.present?
+ json.contact do
+ json.partial! 'api/v2/contributors/show', contributor: presenter.data_contact,
+ is_contact: true
+ end
+end
+
+unless @minimal
+ if presenter.contributors.any?
+ json.contributor presenter.contributors do |contributor|
+ json.partial! 'api/v2/contributors/show', contributor: contributor,
+ is_contact: false
+ end
+ end
+
+ if presenter.costs.any?
+ json.cost presenter.costs do |cost|
+ json.partial! 'api/v2/plans/cost', cost: cost
+ end
+ end
+
+ json.project [plan] do |pln|
+ json.partial! 'api/v2/plans/project', plan: pln
+ end
+
+ outputs = plan.research_outputs.any? ? plan.research_outputs : [plan]
+
+ json.dataset outputs do |output|
+ json.partial! "api/v2/datasets/show", output: output
+ end
+
+ json.extension [plan.template] do |template|
+ json.set! :dmproadmap do
+ json.template do
+ json.id template.id
+ json.title strip_tags(template.title)
+ end
+ end
+
+ if @complete
+ json.complete_plan do
+ q_and_a = presenter.complete_plan_data
+ next if q_and_a.blank?
+
+ json.array! q_and_a do |item|
+ json.question_id item[:id]
+ json.title item[:title]
+ json.section strip_tags(item[:section])
+ json.question sanitize(item[:question])
+ json.answer sanitize(item[:answer])
+ end
+ end
+ end
+ end
+end
diff --git a/app/views/api/v2/plans/index.json.jbuilder b/app/views/api/v2/plans/index.json.jbuilder
new file mode 100644
index 0000000000..f19f41d1dc
--- /dev/null
+++ b/app/views/api/v2/plans/index.json.jbuilder
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+json.partial! 'api/v2/standard_response', total_items: @total_items
+
+json.items @items do |item|
+ json.dmp do
+ json.partial! 'api/v2/plans/show', plan: item
+ end
+end
diff --git a/app/views/api/v2/templates/index.json.jbuilder b/app/views/api/v2/templates/index.json.jbuilder
new file mode 100644
index 0000000000..6a29c6e3f4
--- /dev/null
+++ b/app/views/api/v2/templates/index.json.jbuilder
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+json.partial! 'api/v2/standard_response', total_items: @total_items
+
+json.items @items do |template|
+ presenter = Api::V2::TemplatePresenter.new(template: template)
+
+ json.dmp_template do
+ json.title strip_tags(presenter.title)
+ json.description sanitize(template.description)
+ json.version template.version
+ json.created template.created_at.to_formatted_s(:iso8601)
+ json.modified template.updated_at.to_formatted_s(:iso8601)
+
+ json.affiliation do
+ json.partial! 'api/v2/orgs/show', org: template.org
+ end
+
+ json.template_id do
+ identifier = Api::V2::ConversionService.to_identifier(context: @application,
+ value: template.id)
+ json.partial! 'api/v2/identifiers/show', identifier: identifier
+ end
+ end
+end
diff --git a/app/views/devise/registrations/_api_token.html.erb b/app/views/devise/registrations/_api_token.html.erb
index 704be320b1..783c3bc9f5 100644
--- a/app/views/devise/registrations/_api_token.html.erb
+++ b/app/views/devise/registrations/_api_token.html.erb
@@ -1,25 +1,12 @@
<%# locals: user %>
+<% v2_token ||= nil %>
-<% api_wikis = Rails.configuration.x.application.api_documentation_urls %>
-
<%= user.api_token %>
+ <% else %>
+ <%= _("Click the button below to generate an API token") %>
+ <% end %>
+ <%= t('doorkeeper.applications.form.error') %>
<%= link_to t('.new'), new_oauth_application_path, class: 'btn btn-secondary', style: "border-radius: 0;" %>
+ +| <%= t('.name') %> | +<%= t('.callback_url') %> | +<%= t('.actions') %> | ++ |
|---|---|---|---|
| + <%= link_to application.name, oauth_application_path(application) %> + | ++ <%= simple_format(application.redirect_uri) %> + | ++ <%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(application), class: 'btn btn-link' %> + | ++ <%= render 'delete_form', application: application %> + | +
+ <%= (local_assigns[:error_response] ? error_response : @pre_auth.error_response).body[:error_description] %> ++
+ <%= raw t('.prompt', client_name: content_tag(:strong, class: 'text-info') { @pre_auth.client.name }) %> +
+ + <% if @pre_auth.scopes.count > 0 %> +<%= t('.able_to') %>:
+ +<%= params[:code] %>
+| <%= t('doorkeeper.authorized_applications.index.application') %> | +<%= t('doorkeeper.authorized_applications.index.created_at') %> | ++ |
|---|---|---|
| <%= application.name %> | +<%= application.created_at.strftime(t('doorkeeper.authorized_applications.index.date_format')) %> | +<%= render 'delete_form', application: application %> | +
- You can sign in with your institutional credentials by clicking the "Sign in with Institutional or Social ID" button, or you can make an account by clicking the "Create an Account" tab. If your institution is not listed, you can create an account under the Digital Research Alliance of Canada, which is open to any users. + <%= _('Available and accessible throughout the world, while being Canada-focused') %> +
++ <%= _('Collaborate on plans by sharing with other users or describing your research team as you need to') %>
++ <%= _('Planning is an integral part of the research data management lifecycle') %> +
+- DMP Assistant is a national, online, bilingual data management planning tool developed by the Digital Research Alliance of Canada (the Alliance) in collaboration with University of Alberta, and the Arbutus team at the University of Victoria to assist researchers in preparing data management plans (DMPs). This tool is freely available to all researchers, and develops a DMP through a series of key data management questions, supported by best-practice guidance and examples. + You can sign in with your institutional credentials by clicking the "Sign in with Institutional or Social ID" button, or you can make an account by clicking the "Create an Account" tab. If your institution is not listed, you can create an account under the Digital Research Alliance of Canada, which is open to any users.
+ })) + %> + <% + section_2 = format(_(%{- DMPs are one of the foundations of good research data management (RDM), an international best practice, and increasingly required by institutions and funders, including the Canadian Tri-Agencies as outlined in their Research Data Management Policy. + DMP Assistant is a national, online, bilingual data management planning tool developed by the Digital Research Alliance of Canada (the Alliance) in + collaboration with University of Alberta, and the Arbutus team at the University of Victoria to assist researchers in preparing data management plans (DMPs). + This tool is freely available to all researchers, and develops a DMP through a series of key data management questions, supported by best-practice guidance and examples.
+ })) + %> - Getting started: + <% + section_3 = format(_(%{ ++ DMPs are documents that form one of the foundations of good research data management (RDM), an international best practice, and increasingly required by institutions and funders, including the Canadian Tri-Agencies as outlined in their + Research Data Management Policy. +
+ })) + %> + <% + section_4 = format(_(%{- For more resources and training materials spanning the entire research data life cycle, see the Alliance Training Resources. + For more resources and training materials spanning the entire research data life cycle, see the + Alliance Training Resources.
+ + }), training_resources_path: training_resources_path) + %> + <% + section_5 = format(_(%{- DMP Assistant was adapted from the Digital Curation Centre (DCC)’s DMPonline tool, and uses the DMP Roadmap codebase developed by DCC and the University of California Curation Center (UC3). + DMP Assistant was adapted from the + Digital Curation Centre (DCC)’s + DMPonline tool, and uses the DMP Roadmap codebase developed by DCC and the + University of California Curation Center (UC3).
DMP Assistant was originally developed for Canada by the Portage Network, then a part of the Canadian Association of Research Libraries (CARL). In 2021, Portage Network and its portfolio of platforms and services merged into the Digital Research Alliance of Canada. In 2023 and 2024, we have undertaken a rebrand of the platform, several templates and the organization previously called Portage have transitioned to Alliance branding.
- }),how_to_manage_your_data_path: how_to_manage_your_data_path, training_resources_path: training_resources_path) - + })) %> +<%= _('DMP Assistant is an all-purpose tool for preparing data management plans (DMPs).') %>
-<%= _('Researchers will be guided through best practices in data stewardship.') %>
- -<%= _('Currently, this tool includes a generic Alliance template for researchers to use. Guidance is provided to help you interpret and answer the questions.') %>
- -- <% email_link = Rails.configuration.branding[:organisation][:helpdesk_email] %> - <% email_subject = _('DMP Assistant feedback').gsub(' ', '%20') %> - <% contact_your_institution_url = contacts_at_your_instutution_path %> - <%= sanitize(_('We want your feedback! If you have suggestions on how to improve the existing template, or you would like to add additional templates based on funding agency requirements or disciplinary needs, please contact your local institution or contact us at %{email_link}.') % {contact_your_institution_url: contact_your_institution_url, email_link: email_link, email_subject: email_subject}) %> + <%= sanitize(_('The DMP Assistant is a national, online, bilingual data management planning tool developed and supported by the Digital Research Alliance of Canada (formerly the Portage Network), with partnerships through the University of Alberta Library (development partner) and Arbutus at the University of Victoria (infrastructure partner) to assist researchers in preparing data management plans (DMPs). The tool can also help organizations manage organizational DMP services through customizable organizational profiles, templates, guidance, and usage reporting.') % + { dmp_url: "https://dmp-pgd.ca" }) %>
-<%= _("If you have an account, please sign in and start creating or editing your DMP.") %>
+<%= _('Researchers will be guided using best practices in data stewardship:') %>
- <% unless user_signed_in? %> -- <% sign_up_url = request.base_url %> - <%= sanitize(_('If you do not have a DMP Assistant account, click on \'Sign up\' on the homepage.') % { sign_up_url: sign_up_url} ) %> -
- -- <%= _('You may sign up for an account as part of your institution, or if you do not have one you may sign up under the Digital Research Alliance of Canada.') %> -
- <% end %> - -- <%= _('DMP Assistant has recently implemented single sign on, which will allow you to use CI Logon to use institutional or social accounts in order to sign into your DMP Assistant account.') %> -
- -- <% help_url = help_path %> - <%= sanitize(_('Please visit the \'Help\' page for guidance.') % {help_url: help_path}) %> -
- -- <%= sanitize(_('If you have any questions or comments about the DMP Assistant, contact your local institution or email us at %{email_link}.') % { contact_your_institution_url: contact_your_institution_url, email_link: email_link, email_subject: email_subject} ) %> -
+- <%= _('The Alliance template is based on internationally accepted standards and best practices. It has been prepared and is maintained by a group of research data management experts from research libraries across Canada. It was developed by the Data Management Planning Expert Group, then part of the Portage Network. Portage became part of the Digital Research Alliance of Canada in 2021.') %> -
+- <%= _('Organizations may provide their own DMP templates. The Alliance is the default DMP. Select templates from other organizations from the drop-down Organization list when creating your plan. Users can make use of templates in any available organization, and collaborate across organizations.') %> + <%= sanitize(_('Launched in 2015, the DMP Assistant was adapted from the Digital Curation Centre (DCC)’s DMPonline tool and uses the DMP Roadmap codebase. DMP Assistant was originally developed for Canada by the Portage Network, then a part of the Canadian Association of Research Libraries (CARL). In 2021, Portage Network and its portfolio of platforms and services merged into the Digital Research Alliance of Canada. DMP Assistant in Canada is administered by the Digital Research Alliance of Canada, with support from development partner the University of Alberta Library and hosted on the Arbutus Cloud at the University of Victoria.') % + { + dcc_url: "https://dcc.ac.uk/", + dmponline_url: "https://dmponline.dcc.ac.uk/", + roadmap_url: "https://github.com/DMPRoadmap/roadmap" + }) %>
-- <%= sanitize(_('For funding agencies, research institutions, and other organizations, please contact %{email_link} if you would like to add your organization templates to DMP Assistant.') % { email_link: email_link}) %> -
+<%= _("When you log in to DMP Assistant you will be directed to the 'My Dashboard' page. From here you can edit, share, download, copy or remove any of your plans. You will also see plans that have been shared with you by others.") %>
-- <% email_link = Rails.configuration.branding[:organisation][:helpdesk_email] %> - <% email_subject = _('DMP Assistant feedback').gsub(' ', '%20') %> - <% contact_your_institution_url = contacts_at_your_instutution_path %> - <% how_to_manage_your_data_url = how_to_manage_your_data_path %> - <%= sanitize(_('If you need assistance in developing your data management plan, the following resource may help get you started: How to manage your data. If you require more guidance, contact us at dmp-assistant@tech.alliancecan.ca.') % {contact_your_institution_url: contact_your_institution_url, email_link: email_link, email_subject: email_subject, how_to_manage_your_data_url: how_to_manage_your_data_url}) %> -
+ ++ <%= _("DMP Assistant provides users with the ability to create Data Management Plans (DMPs). In order to create a DMP within DMP Assistant, users will need to have created an account. Users can sign up for an account using their organization, or if they don’t belong to one, may sign up under the Digital Research Alliance of Canada. DMP Assistant also has the option of signing in using single sign on, which enables users to use CI Logon to sign in using institutional or social accounts.") %> +
++ <%= _("Once users have logged in, they can create DMPs by selecting a template under the ‘Create Plan’ tab. Templates are designed for users to create DMPs by guiding users to answer the questions required of a DMP. There are specialized templates that may be useful, depending on the research project. All Alliance templates are available, but local templates may also be available depending on the user’s organization.") %> +
-- <% sso_english_guidelines_url = 'https://drive.google.com/file/d/11m2lepJF7207uwqKwR0TzWDcPAZyq6Kb/view?usp=drive_link'%> - <% sso_french_guidelines_url = 'https://drive.google.com/file/d/1QIVhN6P566xUaOyVZMwtfz8a7fI4X1D_/view?usp=drive_link'%> - <%= sanitize(_('For help with single sign-on, help documents are available in English and in French.') % - {sso_french_guidelines_url: sso_french_guidelines_url, sso_english_guidelines_url: sso_english_guidelines_url } )%> -
- -+ <%= _("If you have any questions that are not addressed here, or have any comments or concerns please email the DMP Assistant Service Team helpdesk at") %> + <%= mail_to Rails.configuration.x.organisation.helpdesk_email %> +
-<%= _("To create a plan, click the 'Create plan' button from the 'My Dashboard' page or the top menu. Select options from the menus and tickboxes to determine what questions and guidance you should be presented with. Confirm your selection by clicking 'Create plan.'") %>
+ +<%= _("The tabbed interface allows you to navigate through different functions when editing your plan.") %>
-<%= _("When viewing any of the question tabs, you will see the different sections of your plan displayed. Click into these in turn to answer the questions. You can format your responses using the text editing buttons.") %>
+<%= _("Guidance is displayed in the right-hand panel. If you need more guidance or find there is too much, you can make adjustments on the ‘Project Details’ tab.") %>
-<%= _("Insert the email address of any collaborators you would like to invite to read or edit your plan. Set the level of permissions you would like to grant them via the radio buttons and click to 'Add collaborator.' Adjust permissions or remove collaborators at any time via the drop-down options.") %>
+<%= _("The ‘Share’ tab is also where you can set your plan visibility.") %>
-<%= _("A DMP is a written document that articulates strategies and tools that you are planning or using to effectively manage your research data. It addresses the management of research data before, during and after the active phases of your research project. It is considered a living document and can be updated and modified as needed throughout your research project.") %>
+<%= _("By default all new and test plans will be set to ‘Private’ visibility. ‘Public’ and ‘Organisational’ visibility are intended for finished plans.") %>
+<%= _("Visit dmp-pgd.ca. You can either create an account by selecting the ‘Create account’ tab and filling in your information, or by selecting ‘Signing in with institutional or social ID’.") %>
+<%= _("Once you’ve created an account, an email confirmation will be sent, and you will need to confirm your email address before accessing the platform. If you require assistance with this step, please contact the help desk.") %>
+<%= _("There may also be an option to request feedback on your plan. This is available when research support staff at your organisation have enabled this service. Click to ‘Request feedback’ and your local administrators will be alerted to your request. Their comments will be visible in the ‘Comments’ field adjacent to each question. You will be notified by email when an administrator provides feedback.") %>
+<%= _("No, DMP Assistant is a free to use platform for researchers and institutions.") %>
+<%= _("If your institution is not one of the available options, you can use the ‘Digital Research Alliance of Canada’, until your organisation has an account created, and then you may change your organisation on your profile page. Please contact your local organisation’s research data management contact to request the organisation be added to the platform.") %>
+<%= _("If your institution does not integrate with CILogin, then unfortunately you will not be able to sign into the platform with your institutional credentials. However, this does not mean that you cannot access the platform, you can select a social sign in, such as ORCID, or create an account directly on the platform.") %>
+<%= _("After signing in or creating an account, you can create a plan by selecting 'Create plan' under the dashboard or on the top header menu.") %>
+<%= _("Provide a title for your research project, this can be changed at a later time as needed. Select your organization from the drop down menu, if your organization is not listed, you can select ‘Digital Research Alliance of Canada’ to access all national DMP templates. If you think your organization should be listed, reach out to your local Research Data Management contact and they can request an organization to be added to the platform.") %>
+<%= _("After selecting your organization, a list of available templates will appear. Your organization may have local templates to use, but if not, all Alliance templates are available to use. Which template you select will depend on your research project. Note: templates may not completely align with your research project and you may need to select the one that best fits your project. Once a template has been selected, you cannot change the template, you will need to create a new plan, selecting a different template.") %>
+<%= _("A template is a designed set of questions and guidance to help guide researchers to develop a data management plan within DMP Assistant.") %>
+<%= _("There are different ways to share a DMP, whether you want to share your plan internally within your team, externally with other researchers or with staff at your local institution.") %>
+<%= _("To share plans with members of your team so that they can view/edit the DMP, under the ‘Share’ tab when writing your DMP, you can invite who you wish to add and assign them a level of permission (co-owner, editor, read-only). You can add as many collaborators as you would like.") %>
+<%= _("You can add contributors to your plan, but this does NOT provide access to view/edit the DMP, but if you want to name contributors such as a PI (Principal Investigator) on the DMP this would be where you can add their names.") %>
+<%= _("Some organisations have the Request Feedback feature enabled. If your organisation utilizes this feature, you will see an extra tab while writing your plan. If you see this tab, you can request feedback and a notification will be sent to your local administrators.") %>
+<%= _("If you do not see this tab, you may still be able to receive feedback from expertise at your organisation, however you will need to reach out to your local contact separately.") %>
+<%= _("Research outputs are optional fields for researchers to complete in addition to the fields of the ‘Write Plan’ component of the DMP. The Research Outputs include fields such as the type of research, title of research, repositories and metadata standards that will be used to share your research, anticipated release date, level of initial access and if applicable and licenses. This is not a required page but may assist researchers in the long term planning of their research project.") %>
+<%= _("Under the download tab of any DMP, there are a number of downloadable formats for users to download their plans, including csv, html, pdf, text, docx, and json. Users can select what components of their DMP they wish to include in the download.") %>
+<%= _("While users themselves cannot make changes to the template, if additional information is required on a DMP, or there is information you want to add to the DMP but not within the platform, users are suggested to download their plan in a workable format (e.g. text or docx), and make the edits outside of the platform.") %>
+<%= _("If you need further assistance with a DMP, please reach out to your local RDM support contact.") %>
+<%= _("Unfortunately an email cannot be changed directly, however you can create a new account with the email address you wish to use. If desired, accounts can be merged, if you wish to do so then contact the help desk and the service team can merge your accounts to ensure you do not lose any of your information.") %>
+<%= _("If you move to another organization or institution, you can still access your account providing you have access to your email.") %>
+<%= _("If you do not have access to your email, you may still be able to access your DMP Assistant account, however you can create a new account and contact the help desk, where the service team can merge your accounts, so you don’t lose access to your information.") %>
+<%= _("Alternatively, if you don’t wish to create a new account and still have access to your email from the past organization or institution, you can change your profile organization. Go to your name within DMP Assistant and select ‘Edit profile’. From there you can select your new organization and save that information.") %>
+<%= _("Contributors can be anyone you work with, and you can enter any name you want. The contributors will be listed in the plan, but cannot edit or modify the plan in any way.") %>
+<%= _("Collaborators are anyone you wish to share the plan with, and will require the person to create or sign into DMP Assistant to collaborate with you. Collaborators have the ability to comment and edit on the plan as needed.") %>
+<%= _("These functions are different as in many cases not all who are listed as contributors are collaborators, and not all collaborators need to be listed as contributors for all projects depending on their role(s) within the project.") %>
+<%= _("If you have signed into your account using single sign on/institutional credentials and are unable to find/view any of your DMPs, you may have created a separate account.") %>
+<%= _("You can either sign in to your account using a different email that may be associated with an account, or you can reach out to the help desk for further assistance. If you unintentionally created an account, we can have the two accounts merged, but this is something that must be done via the service team help desk.") %>
+<%= _("If you receive this message, you must contact the help desk. This is a known issue and users may receive this message when their language is improperly stored in the database. Once the language has been reset by the service team, you should be able to sign in normally.") %>
+<%= _("From here you can download your plan in various formats. This may be useful if you need to submit your plan as part of a grant application. Choose what format you would like to view/download your plan in and click to download. You can also adjust the formatting (font type, size and margins) for PDF files, which may be helpful if working to page limits.") %>