diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 79ecfeb7a9..af1fab23a8 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -9,6 +9,9 @@ jobs: steps: - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 - name: Install modules run: yarn - name: Run ESLint diff --git a/.github/workflows/mysql.yml b/.github/workflows/mysql.yml index 418354b21d..ec194cad36 100644 --- a/.github/workflows/mysql.yml +++ b/.github/workflows/mysql.yml @@ -1,6 +1,6 @@ name: Tests - MySQL -on: [pull_request] +on: [push, pull_request] jobs: mysql: @@ -16,6 +16,9 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 1 + - uses: actions/setup-node@v3 + with: + node-version: 16 - name: 'Install MySQL Packages' run: | @@ -62,7 +65,7 @@ jobs: # generate a default credential file and key EDITOR='echo "$(cat config/credentials.yml.example)" >' bundle exec rails credentials:edit - # Try to retrieve the yarn JS dependencies from the cache + # # Try to retrieve the yarn JS dependencies from the cache - name: 'Cache Yarn Packages' uses: actions/cache@v2.1.5 with: @@ -97,6 +100,7 @@ jobs: bin/rails webpacker:compile bin/rails assets:precompile + # Note V3.1.0 DMPTool commented out Karma tests and will move this part to rspec - name: 'Run Karma Tests' run: yarn test diff --git a/.github/workflows/postgres.yml b/.github/workflows/postgres.yml index 3d340eefda..1a5d3852f3 100644 --- a/.github/workflows/postgres.yml +++ b/.github/workflows/postgres.yml @@ -32,6 +32,9 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 1 + - uses: actions/setup-node@v3 + with: + node-version: 16 - name: 'Install Postgresql Packages' run: | diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index e736a45646..f650d13435 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -27,6 +27,9 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 1 + - uses: actions/setup-node@v3 + with: + node-version: 16 - name: 'Install MySQL Packages' run: | diff --git a/.gitignore b/.gitignore index 4e4f391fcc..f9b1418bbf 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,8 @@ coverage # config/branding.yml # Ignore some of the initializers +# 3.1.0 +config/initializers/wicked_pdf.rb config/initializers/fingerprint.rb # Ignore enviroments settings @@ -86,7 +88,8 @@ yarn-error.log yarn-debug.log* .env -.env-working +# integration +.env-working package-lock.json node_modules /public/packs @@ -110,3 +113,6 @@ yarn-debug.log* /yarn-error.log yarn-debug.log* .yarn-integrity + +# Ignore staging files in db folder since they can be auto-generated +/db/seeds/staging \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml index af6bbac1ba..66853ae6ff 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,179 +1,195 @@ +# ---------------- +# - INSTRUCTIONS - +# ---------------- +# The DMPRoadmap codebase tries to follow the latest Ruby/Rails style guidelines as defined +# by the community via the Rubocop gem. +# +# Before submitting a PR, please run `bin/rubocop` from the project root. +# Note that you can specify individual files or folders e.g.: `bin/rubocop app/mailers` +# Note you can let Rubocop auto-correct many issues with the `-a` flag +# +# New versions of Rubocop typically include new Cops (Cops are inidivual Rubocop rules). +# If you see a message like the following when you run `bin/rubocop`: +# +# "The following cops were added to RuboCop, but are not configured. Please set Enabled +# to either `true` or `false` in your `.rubocop.yml` file." +# +# You should copy and paste the specified Cops into this file. You can review what the +# Cop will do by Googling the name of the rule e.g.: "rubocop Layout/SpaceBeforeBrackets" +# +# After you review the rule, you can either Enable it or Disable it in this file. The +# Rubocop documentation for the Cop may also give you additional options that can be +# configured. +# +# Try to place any new Cops under their relevant section and in alphabetical order + AllCops: - # Cache the results for faster processing - UseCache: true # Show the name of the cops being voilated in the feedback DisplayCopNames: true DisplayStyleGuide: true + + # Rubocop will skip checking the following directories Exclude: - 'bin/**/*' - 'db/**/*' - 'vendor/**/*' - 'node_modules/**/*' - - 'test/**/*' - - 'lib/tasks/*' + - 'scripts/**/*' -# Force no empty lines at the start or end of a block's body. Ignore specs, since this -# improves readability within the RSpec blocks. -Layout/EmptyLinesAroundBlockBody: - Exclude: - - 'spec/**/*' + # Automatically add any new Cops to this file and enable them + NewCops: enable -# Force a single blank line around a class's body. Adding this whitespace makes code -# a bit easier to read. -Layout/EmptyLinesAroundClassBody: - Enabled: true - EnforcedStyle: empty_lines - -# Force a single blank line around a module's body. Adding this whitespace makes code -# a bit easier to read. -Layout/EmptyLinesAroundModuleBody: - Enabled: true - EnforcedStyle: empty_lines + # Cache the results for faster processing + UseCache: true -# Ignore this cop. The Rubocop default is sensible, but the rubocop-rails gem modifies -# this to position end keywords awkwardly. -Layout/EndAlignment: +# ----------- +# - GEMSPEC - +# ----------- +Gemspec/DeprecatedAttributeAssignment: Enabled: true - EnforcedStyleAlignWith: keyword - -# The difference between `rails` and `normal` is that the `rails` style -# prescribes that in classes and modules the `protected` and `private` -# modifier keywords shall be indented the same as public methods and that -# protected and private members shall be indented one step more than the -# modifiers. Other than that, both styles mean that entities on the same -# logical depth shall have the same indentation. -Layout/IndentationConsistency: - Description: 'Keep indentation straight.' - StyleGuide: '#spaces-indentation' - Enabled: true - EnforcedStyle: normal -Layout/IndentationWidth: - Description: 'Use 2 spaces for indentation.' - StyleGuide: '#spaces-indentation' +# ---------- +# - LAYOUT - +# ---------- +Layout/LineEndStringConcatenationIndentation: # new in 1.18 Enabled: true - -# Restrict the length of each line of code to 90 characters. Enforcing this is important -# as many developers are working on smaller screens, or split screens. Having to scroll -# to read a full line of code makes code harder to read and more frustrating to work with. -Layout/LineLength: - # I've found that 90 is a suitable limit. Many developers balk at the 80 character - # default. - Max: 100 - -Layout/EmptyLinesAroundAttributeAccessor: +Layout/SpaceBeforeBrackets: # new in 1.7 Enabled: true -Layout/SpaceAroundMethodCallOperator: +# -------- +# - LINT - +# -------- +Lint/AmbiguousAssignment: # new in 1.7 Enabled: true - -# Enforce this in the main code but ignore it in specs since the Rspec core methods -# are defined as potentially ambiguous blocks Lint/AmbiguousBlockAssociation: Exclude: - 'spec/**/*' - -Lint/DeprecatedOpenSSLConstant: +Lint/AmbiguousOperatorPrecedence: # new in 1.21 Enabled: true - -Lint/MixedRegexpCaptureTypes: +Lint/AmbiguousRange: # new in 1.19 Enabled: true - -Lint/RaiseException: +Lint/DeprecatedConstants: # new in 1.8 Enabled: true - -Lint/StructNewOverride: +Lint/DuplicateBranch: # new in 1.3 + Enabled: true +Lint/DuplicateRegexpCharacterClassElement: # new in 1.1 + Enabled: true +Lint/EmptyBlock: # new in 1.1 + Enabled: true +Lint/EmptyClass: # new in 1.3 + Enabled: true +Lint/EmptyInPattern: # new in 1.16 + Enabled: true +Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21 + Enabled: true +Lint/LambdaWithoutLiteralBlock: # new in 1.8 + Enabled: true +Lint/NoReturnInBeginEndBlocks: # new in 1.2 + Enabled: true +Lint/NumberedParameterAssignment: # new in 1.9 + Enabled: true +Lint/OrAssignmentToConstant: # new in 1.9 + Enabled: true +Lint/RedundantDirGlobSort: # new in 1.8 + Enabled: true +Lint/RequireRelativeSelfPath: # new in 1.22 + Enabled: true +Lint/SymbolConversion: # new in 1.9 + Enabled: true +Lint/ToEnumArguments: # new in 1.1 + Enabled: true +Lint/TripleQuotes: # new in 1.9 + Enabled: true +Lint/UnexpectedBlockArity: # new in 1.5 + Enabled: true +Lint/UnmodifiedReduceAccumulator: # new in 1.1 + Enabled: true +Lint/Debugger: # new in 1.45.0 + Description: 'Check for debugger calls.' Enabled: true - -# Bumping the default AbcSize so we don't need to refactor everything -Metrics/AbcSize: - Max: 25 - -# Restrict the number of lines of code that may be within a block of code. This should -# force developers to break their code into smaller discrete methods or objects. -Metrics/BlockLength: - # Exclude specs, since those are defined as large blocks of code Exclude: - - 'spec/**/*' - -# Bumping the default ClassLength so we don't need to refactor everything + - 'lib/tasks/**/*' + +# ----------- +# - METRICS - +# ----------- +# briley Oct. 4th 2021 +# Default is 100 lines. Most of our controllers, models, etc. violate this +# Cop, so setting it to 300 since we do not have time to refactor everything Metrics/ClassLength: Max: 300 - -# Bumping the default CyclomaticComplexity so we don't need to refactor everything -Metrics/CyclomaticComplexity: - Max: 25 - -# Bumping the default MethodLength so we don't need to refactor everything +# briley Oct. 4th 2021 +# Default is 10 lines which feels very restrictive but would also require us to do +# too much refactoring at this point. Metrics/MethodLength: - Max: 25 - -# Bumping the default PerceivedComplexity so we don't need to refactor everything -Metrics/PerceivedComplexity: - Max: 25 + Max: 20 -# This cop enforces the use of boolean and/or "&&" and "||" over "and" "or". -# Sometimes using "and"/"or" is preferrable, when these are used as control flow. -# -# For example: -# -# render text: "Hello world" and return -# -Style/AndOr: - Enabled: false - -# This cop enforces how modules and classes are nested within another module or class. -# In Rails code (e.g. models and controllers) nesting with a colon is preferrable (e.g. -# User::Session). -Style/ClassAndModuleChildren: +# mnicholson Oct. 6th 2021 +# Default lenght for block is 25 lines, which it would be very restrictive for +# the Rspec views methods. So I'll just exclude some files. +Metrics/BlockLength: Exclude: - - 'app/**/*' + - 'lib/tasks/*.rake' + - 'lib/tasks/utils/*.rake' + - 'spec/**/*' -# This cop enforces each class to have documentation at the top. That's not always -# practical or necessary in Rails apps (e.g. the purpose of helpers is self evident). -Style/Documentation: - Enabled: false - -# Enforce empty methods to be written across two lines, like any normal method would be. -# This allows for easy modification of the method in future. -Style/EmptyMethod: + AllowedMethods: ['describe', 'context', 'task', 'namespace'] +# ------------ +# - SECURITY - +# ------------ +Security/IoMethods: # new in 1.22 Enabled: true - EnforcedStyle: expanded - -# Leave the string formatting style as `"some text %{value}" % { value: "text" }` -# since we're uncertain what effect `format` and `sprintf` may have on the Fastgetext -# markup `_("text")` -Style/FormatString: - EnforcedStyle: percent -# Prefer the use of `"some %{token} text"` instead of `some % text` or -# `some %token text` since it would invalidate many of our translation strings +# --------- +# - STYLE - +# --------- +Style/ArgumentsForwarding: # new in 1.1 + Enabled: true +Style/CollectionCompact: # new in 1.2 + Enabled: true +Style/DocumentDynamicEvalDefinition: # new in 1.1 + Enabled: true +Style/EndlessMethod: # new in 1.8 + Enabled: true Style/FormatStringToken: + # Force use of the `%{variable}` style of tokens instead of `%s` because + # Translation.io has trouble with auto-translating it. It converts `%s` to + # `% s` (note the added space) EnforcedStyle: template - -# Enforce double quotes. Don't allow single quotes. This is preferred since double -# quotes are more useful (they support escaping characters, and interpolation). -Style/StringLiterals: +Style/HashConversion: # new in 1.10 Enabled: true - EnforcedStyle: double_quotes - -Style/ExponentialNotation: +Style/HashExcept: # new in 1.7 Enabled: true - -Style/HashEachMethods: +Style/IfWithBooleanLiteralBranches: # new in 1.9 Enabled: true - -Style/HashTransformKeys: +Style/InPatternThen: # new in 1.16 Enabled: true - -Style/HashTransformValues: +Style/MultilineInPatternThen: # new in 1.16 Enabled: true - -Style/RedundantRegexpCharacterClass: +Style/NegatedIfElseCondition: # new in 1.2 Enabled: true - -Style/RedundantRegexpEscape: +Style/NilLambda: # new in 1.3 Enabled: true - -Style/SlicingWithRange: +Style/NumberedParameters: # new in 1.22 + Enabled: true +Style/NumberedParametersLimit: # new in 1.22 + Enabled: true +Style/OpenStructUse: + Enabled: false # used heavily in API so needs a lot of work to refactor +Style/QuotedSymbols: # new in 1.16 + Enabled: true +Style/RedundantArgument: # new in 1.4 + Enabled: true +Style/RedundantSelfAssignmentBranch: # new in 1.19 + Enabled: true +Style/SelectByRegexp: # new in 1.22 + Enabled: true +Style/StringChars: # new in 1.12 + Enabled: true +Style/StringLiterals: + Enabled: true + Exclude: + - 'app/views/**/*' + - 'config/**/*' +Style/SwapValues: # new in 1.1 Enabled: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 853e0524f3..f9b89a5218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,134 @@ # Changelog +## [3.1.0+portage-3.1.0] - 2023-02-22 + +### Added + +- In the project detail page, users can now specify a primary research domain for their project (e.g. Biology, Computer Science, Humanities, etc.). This information appears in the DMP's JSON as a 'tag' + +- On the project detail page, users can now indicate and describe any ethical concerns that concern their research data. Clicking the checkbox will display a field to describe the concerns and a URL field to link to a report. This information appears in the DMP's JSON. + +- Added a new 'Research Outputs' tab that allows users to specify information about their intended research outputs [#2738](https://github.com/portagenetwork/DMPRoadmap/issues/2738). User can specify the type (e.g. dataset, software, image, etc.), name and a description of the output, size (if applicable), whether or not it contains sensitive info or PII, what the initial access level will be (e.g. open, restricted, closed) and the anticipated publication date: + +- ![Research Output](https://user-images.githubusercontent.com/1204467/162054150-e58ec156-18bb-4c2f-a516-1f28e14c3204.png "Research Outputs") + +- Allowed user to select an appropriate license in the Research Outputs tab (note that the options come from the [SPDX license registry](https://spdx.org/licenses/)) [#2607](https://github.com/portagenetwork/DMPRoadmap/issues/2607) + +- Allowed user to any metadata standards that will be followed (e.g. Dublin Core) in the Research Outputs tab (note that the list of standards comes from the [RDA Metadata standards catalog](https://rdamsc.bath.ac.uk/)) [#2798](https://github.com/portagenetwork/DMPRoadmap/issues/2798) + +- Allowed users to select the repositories they intend to preserve the object in (e.g. Dryad, Zenodo, GitHub, or an institutional repository) in the Research Outputs tab (note that the list of repositories comes from the [re3data registry](https://www.re3data.org/)) [#2605](https://github.com/portagenetwork/DMPRoadmap/issues/2605) + +- Added research outputs result to downloaded PDF file in a table format [#2812](https://github.com/portagenetwork/DMPRoadmap/issues/2812) + +- Updated APIs to add research outputs result to the downloaded JSON file, which appears as 'Dataset' information + +- Added helpdesk email address field to `Organisation's details`. This email will be used in the email signature for emails sent from the system (assuming the recipient of the email is a member of the same Organisation) [#3140](https://github.com/portagenetwork/DMPRoadmap/issues/3140) + +- Added data migration rake tasks and migrated DMP Assistant database from MariaDB 10 to PostgreSQL 12 to better accommodate JSON format data + +- Sandbox testing flag can be turned on/off so org admin can change *locally hosted* DMP Assistant to a sandbox testing environment + +- Added rubocop test and code side support [#264](https://github.com/portagenetwork/roadmap/issues/264) + +- Updated rubocop setting to disable the debug checking after upgrading to rubocop 1.45 [#316](https://github.com/portagenetwork/roadmap/issues/316) + +- Added more clear error message to remind users not to fresh the page when the 'saving plan' process is stuck and the success message is not shown as expected [#241](https://github.com/portagenetwork/roadmap/issues/241) + +- Added static page for server upgrading time [#310](https://github.com/portagenetwork/roadmap/issues/310) + +- Make the comments/guidances are collapsible to give the researcher more space to see their answers [#3123](https://github.com/portagenetwork/DMPRoadmap/issues/3123) + +### Changed + +- DMP Assistant migrated all data from MariaDB 10 to PostgreSQL 12 + +- Updated all Gems and JS dependencies + +- Used excel sheet to track the upgrading process [#187](https://github.com/portagenetwork/roadmap/issues/187) + +- Updated the french translations for the Portage template for systematic reviews [#269](https://github.com/portagenetwork/roadmap/issues/269) + +- Adjusted Dockerfile.Production to move `rails assets:precompile` step to `docker-compose.yml` file [#282](https://github.com/portagenetwork/roadmap/issues/282) + +- Relived memory load in the translation syncing process by adjusting configuration so that only code in the `app` folder will be scanned and translated [#313](https://github.com/portagenetwork/roadmap/issues/313) + +- Adjusted customized translation.io source code to add `unscoped` method for the necessary table to fit PostgreSQL syntax (https://github.com/lagoan/translation_io_rails/pull/2) + +### Fixed + +- Reversed `perm_helper.rb` changes to reduce translation.io sync memory pressure and added rubocop exception for `translation.rb` [#315](https://github.com/portagenetwork/roadmap/issues/315) + +- Unmanaged organization will not show in all org selectors except the creating new organization page [#260](https://github.com/portagenetwork/roadmap/issues/260) + +- Allowed contributors to choose organizations [#273](https://github.com/portagenetwork/roadmap/issues/273) + +- Allowed admin to see unmanaged org name when creating new org to avoid duplication [#275](https://github.com/portagenetwork/roadmap/issues/275) + +- Force Github actions using Ubuntu 20.04 to fix wkhtmltopdf-binary issue [#266](https://github.com/portagenetwork/roadmap/issues/266) + +- Clarified of String and Array behavior for plan exports [#268](https://github.com/portagenetwork/roadmap/issues/268) + +- Fixed the error that `app.pot` headers accidentals appear in the empty text box of *writing plans* area [#308](https://github.com/portagenetwork/roadmap/issues/308) + +- Fixed for pagination of plans retrieved using the REST API V0 [#3105](https://github.com/portagenetwork/DMPRoadmap/issues/3105) + +- Security vulnerability patches [#3100](https://github.com/portagenetwork/DMPRoadmap/issues/3100), [#3097](https://github.com/portagenetwork/DMPRoadmap/issues/3097), [#3093](https://github.com/portagenetwork/DMPRoadmap/issues/3093), [#3077](https://github.com/portagenetwork/DMPRoadmap/issues/3077) + +- Fixed issues/inconsistencies with policies [#3084](https://github.com/portagenetwork/DMPRoadmap/issues/3084) [#3099](https://github.com/portagenetwork/DMPRoadmap/issues/3099) + +- Fix for issue downloading PDFs [#3054](https://github.com/portagenetwork/DMPRoadmap/issues/3054)[#3049](https://github.com/portagenetwork/DMPRoadmap/issues/3049) [#3098](https://github.com/portagenetwork/DMPRoadmap/issues/3098) + +- Fix for issue displaying users as collaborators on a plan after they have removed themselves Fix for issue downloading PDFs [#3095](https://github.com/portagenetwork/DMPRoadmap/issues/3095) + +- Fix for API v0 not supplying the owner email address Fix for issue downloading PDFs [#3094](https://github.com/portagenetwork/DMPRoadmap/issues/3094) + +- Fix for bug preventing Super Admins from creating new Orgs [#3091](https://github.com/portagenetwork/DMPRoadmap/issues/3091) + +- Fix for issue causing deleted plans to appear in the Org Admin list of plans [#3087](https://github.com/portagenetwork/DMPRoadmap/issues/3087) + +- Fix for CSV downloads [#3085](https://github.com/portagenetwork/DMPRoadmap/issues/3085)[#3075](https://github.com/portagenetwork/DMPRoadmap/issues/3075) + +- Fix for pagination and search on the Admin plans page [#3069](https://github.com/portagenetwork/DMPRoadmap/issues/3069) [#3073](https://github.com/portagenetwork/DMPRoadmap/issues/3073) + +- Fix for strings that were not included in translation content [#3081](https://github.com/portagenetwork/DMPRoadmap/issues/3081)[#3050](https://github.com/portagenetwork/DMPRoadmap/issues/3050) + +- Fix for issue with contributor org logic not adhering to the restrict_orgs config flag [#3078](https://github.com/portagenetwork/DMPRoadmap/issues/3078)[#3060](https://github.com/portagenetwork/DMPRoadmap/issues/3060) + +- Fix for adding contributors [#3071](https://github.com/portagenetwork/DMPRoadmap/issues/3071) + +- Removed phone number field from contributor page [#3067](https://github.com/portagenetwork/DMPRoadmap/issues/3067) + +- Fixed issue with merging users [#3065](https://github.com/portagenetwork/DMPRoadmap/issues/3065) + +- Fixed issue with emails [#3052](https://github.com/portagenetwork/DMPRoadmap/issues/3052) + +- Fix for users unable to see API page [#3047](https://github.com/portagenetwork/DMPRoadmap/issues/3047) + +- Upgraded Rubocop and updated all files accordingly [#3048](https://github.com/portagenetwork/DMPRoadmap/issues/3048) [#3045](https://github.com/portagenetwork/DMPRoadmap/issues/3045) + +- Fixed an issue that was preventing Org Admins from seeing the 'Share' tab [#3131](https://github.com/portagenetwork/DMPRoadmap/issues/3131) + +- Fixed an issue that was causing errors when Org Admins tried to save changes to a template [#3071](https://github.com/portagenetwork/DMPRoadmap/issues/3038) + +- Patched an issue that continued to show users as collaborators on a Plan's Share tab even after they had removed themselves from the Plan [#3126](https://github.com/portagenetwork/DMPRoadmap/issues/3126) + +- Fixed an issue that was causing TinyMCE editors to have an incorrect size when they initially load [#3141](https://github.com/portagenetwork/DMPRoadmap/issues/3141) + +- Various fixes for the APIs + +- Patched issue that was always using the default template when creating a new DMP in API v1 [#3137](https://github.com/portagenetwork/DMPRoadmap/issues/3137) + +- Fixed an issue that was preventing plans from being created via API v0 [#3135](https://github.com/portagenetwork/DMPRoadmap/issues/3135) + +- Addressed an issue that was preventing users from being unassigned from a department in API v0 [#3132](https://github.com/portagenetwork/DMPRoadmap/issues/3132) + +- Reverted change to our use of string format tokens that were made during an upgrade of Rubocop. The preferred format is once again Some %{variable_name} text [#3138](https://github.com/portagenetwork/DMPRoadmap/issues/3128) + +- Fixed some potential PDF downloading problems + +- Updated Github Actions to use specified node version [#319](https://github.com/portagenetwork/roadmap/issues/319) + + ## [3.0.4+portage-3.0.16] - 2022-12-14 ### Changed diff --git a/Dangerfile b/Dangerfile index a03cdb145a..f99b7d9a87 100644 --- a/Dangerfile +++ b/Dangerfile @@ -1,11 +1,13 @@ +# frozen_string_literal: true + # Make sure non-trivial amounts of code changes come with corresponding tests has_app_changes = !git.modified_files.grep(/lib/).empty? || !git.modified_files.grep(/app/).empty? has_test_changes = !git.modified_files.grep(/spec/).empty? -if git.lines_of_code > 50 && has_app_changes && !has_test_changes - warn('There are code changes, but no corresponding tests. '\ - 'Please include tests if this PR introduces any modifications in '\ - 'behavior.', +if git.lines_of_code > 50 && has_app_changes && !has_test_changes + warn('There are code changes, but no corresponding tests. ' \ + 'Please include tests if this PR introduces any modifications in ' \ + 'behavior.', sticky: false) end @@ -17,11 +19,14 @@ warn('Please add a detailed summary in the description.') if github.pr_body.leng warn('This PR is too big! Consider breaking it down into smaller PRs.') if git.lines_of_code > 500 # Make it more obvious that a PR is a work in progress and shouldn't be merged yet -warn("PR is classed as Work in Progress") if github.pr_title.include? "[WIP]" +warn('PR is classed as Work in Progress') if github.pr_title.include? '[WIP]' # Let people say that this isn't worth a CHANGELOG entry in the PR if they choose -declared_trivial = (github.pr_title + github.pr_body).include?("#trivial") || !has_app_changes +declared_trivial = (github.pr_title + github.pr_body).include?('#trivial') || !has_app_changes -if !git.modified_files.include?("CHANGELOG.md") && !declared_trivial - failure("Please include a CHANGELOG entry. \nYou can find it at [CHANGELOG.md](https://github.com/portagenetwork/roadmap/blob/deployment-portage/CHANGELOG.md).", sticky: false) -end \ No newline at end of file +if !git.modified_files.include?('CHANGELOG.md') && !declared_trivial + raise( + 'Please include a CHANGELOG entry. You can find it at [CHANGELOG.md](https://github.com/portagenetwork/roadmap/blob/deployment-portage/CHANGELOG.md).', + sticky: false + ) +end diff --git a/Dockerfile.production b/Dockerfile.production index 74622f4875..b19313aae6 100644 --- a/Dockerfile.production +++ b/Dockerfile.production @@ -8,7 +8,7 @@ ENV BUNDLE_PATH=/bundle/ \ ENV Path="${BUNDLE_BIN}:${PATH}" RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ - curl -sL https://deb.nodesource.com/setup_12.x | bash && \ + curl -sL https://deb.nodesource.com/setup_16.x | bash && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list # Dependancies @@ -34,11 +34,11 @@ ENV INSTALL_PATH $INSTALL_PATH WORKDIR $INSTALL_PATH RUN gem install bundler - COPY Gemfile* $INSTALL_PATH +# for 3.1.0 test, need both mysql and psql group installed RUN bundle config --local build.sassc --disable-march-tune-native -RUN bundle config set --local without 'development test pgsql thin aws ci' +RUN bundle config set --local without 'development pssql test thin aws ci' RUN RAILS_ENV=production bundle install --jobs=3 --retry=3 COPY . $INSTALL_PATH diff --git a/Gemfile b/Gemfile index 60276d5670..2c3f2718e3 100644 --- a/Gemfile +++ b/Gemfile @@ -1,39 +1,39 @@ # frozen_string_literal: true -source "https://rubygems.org" +source 'https://rubygems.org' -ruby ">= 2.6.3" +ruby '>= 2.6.3' # ===========# # CORE RAILS # # ===========# # Full-stack web application framework. (http://rubyonrails.org) -gem "rails", "~> 5.2" +gem 'rails', '~> 5.2' # TODO: Remove this once Rails addresses the issue with its dependency on mimemagic. Mimemagic had # an MIT license but was using some incompatible GPL license code. # Versions of mimemagic that were yanked: https://rubygems.org/gems/mimemagic/versions # Analysis of the issue: https://www.theregister.com/2021/03/25/ruby_rails_code/ -gem "mimemagic", "~> 0.3.7" +gem 'mimemagic', '~> 0.3.7' # Use sqlite3 as the database for Active Record # gem 'sqlite3', '~> 1.4' # Use Puma as the app server -gem "puma", group: :puma, require: false +gem 'puma', group: :puma, require: false # Use SCSS for stylesheets -gem "sass-rails" +gem 'sass-rails' # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker -gem "webpacker" +gem 'webpacker' # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks -gem "turbolinks" +gem 'turbolinks' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder -gem "jbuilder" +gem 'jbuilder' # Use Redis adapter to run Action Cable in production # gem "redis", "~> 4.0" @@ -44,7 +44,7 @@ gem "jbuilder" # gem "image_processing", "~> 1.2" # Reduces boot times through caching; required in config/boot.rb -gem "bootsnap", require: false +gem 'bootsnap', require: false # GEMS ADDED TO HELP HANDLE RAILS MIGRATION FROM 3.x to 4.2 # THESE GEMS HELP SUPPORT DEPRACATED FUNCTIONALITY AND WILL LOSE SUPPORT IN @@ -60,26 +60,28 @@ gem "bootsnap", require: false # Rollbar-gem is the SDK for Ruby apps and includes support for apps using # Rails, Sinatra, Rack, plain Ruby, and other frameworks. -gem "rollbar", group: :rollbar, require: false +gem 'rollbar', group: :rollbar, require: false # ======== # # DATABASE # # ======== # +# 3.1.0: configuration may need from Neil's side: bundle config set --local without mysql2 with pg + # A simple, fast Mysql library for Ruby, binding to libmysql # (http://github.com/brianmario/mysql2) -gem "mysql2", group: :mysql, require: false +gem 'mysql2', group: :mysql, require: false # Pg is the Ruby interface to the {PostgreSQL # RDBMS}[http://www.postgresql.org/](https://bitbucket.org/ged/ruby-pg) -gem "pg", group: :pgsql, require: false +gem 'pg', group: :pgsql, require: false # A null db adapter so we can perform tast rake assets:precompile with # production environment without a live database. -gem "activerecord-nulldb-adapter", group: :nulldb, require: false +gem 'activerecord-nulldb-adapter', group: :nulldb, require: false # Bit fields for ActiveRecord (https://github.com/pboling/flag_shih_tzu) -gem "flag_shih_tzu" # , "~> 0.3.23" +gem 'flag_shih_tzu' # , "~> 0.3.23" # ======== # # SECURITY # @@ -87,35 +89,35 @@ gem "flag_shih_tzu" # , "~> 0.3.23" # Flexible authentication solution for Rails with Warden # (https://github.com/plataformatec/devise) -gem "devise" +gem 'devise' # An invitation strategy for Devise (https://github.com/scambra/devise_invitable) -gem "devise_invitable" +gem 'devise_invitable' # A generalized Rack framework for multiple-provider authentication. # (https://github.com/omniauth/omniauth) -gem "omniauth" +gem 'omniauth' # OmniAuth Shibboleth strategies for OmniAuth 1.x -gem "omniauth-shibboleth" +gem 'omniauth-shibboleth' # ORCID OAuth 2.0 Strategy for OmniAuth 1.0 # (https://github.com/datacite/omniauth-orcid) -gem "omniauth-orcid" +gem 'omniauth-orcid' # This gem provides a mitigation against CVE-2015-9284 (Cross-Site Request # Forgery on the request phase when using OmniAuth gem with a Ruby on Rails # application) by implementing a CSRF token verifier that directly uses # ActionController::RequestForgeryProtection code from Rails. # https://nvd.nist.gov/vuln/detail/CVE-2015-9284 -gem "omniauth-rails_csrf_protection" +gem 'omniauth-rails_csrf_protection' # A ruby implementation of the RFC 7519 OAuth JSON Web Token (JWT) standard. -gem "jwt" +gem 'jwt' # Gems for repository integration # OO authorization for Rails (https://github.com/elabs/pundit) -gem "pundit" +gem 'pundit' # ========== # # UI / VIEWS # @@ -124,22 +126,22 @@ gem "pundit" # Ruby gem to handle settings for ActiveRecord instances by storing them as # serialized Hash in a separate database table. Namespaces and defaults # included. (https://github.com/ledermann/rails-settings) -gem "ledermann-rails-settings" +gem 'ledermann-rails-settings' # Gem providing simple Contact Us functionality with a Rails 3+ Engine. # (https://github.com/jdutil/contact_us) -gem "contact_us" # COULD BE EASILY REPLACED WITH OUR OWN CODE +gem 'contact_us' # COULD BE EASILY REPLACED WITH OUR OWN CODE # Helpers for the reCAPTCHA API (http://github.com/ambethia/recaptcha) -gem "recaptcha" +gem 'recaptcha' # Ideal gem for handling attachments in Rails, Sinatra and Rack applications. # (http://github.com/markevans/dragonfly) -gem "dragonfly" +gem 'dragonfly' group :aws do # Amazon AWS S3 data store for use with the Dragonfly gem. - gem "dragonfly-s3_data_store" + gem 'dragonfly-s3_data_store' end # ========== # @@ -148,21 +150,21 @@ end # A pagination engine plugin for Rails 4+ and other modern frameworks # (https://github.com/kaminari/kaminari) -gem "kaminari" +gem 'kaminari' # Paginate in your headers, not in your response body. This follows the # proposed RFC-8288 standard for Web linking. -gem "api-pagination" +gem 'api-pagination' # =========== # # STYLESHEETS # # =========== # # Integrate SassC-Ruby into Rails. (https://github.com/sass/sassc-rails) -gem "sassc-rails" +gem 'sassc-rails' # Font-Awesome SASS (https://github.com/FortAwesome/font-awesome-sass) -gem "font-awesome-sass", "~> 5.13.0" +gem 'font-awesome-sass', '~> 5.13.0' # Use webpack to manage app-like JavaScript modules in Rails # (https://github.com/rails/webpacker) @@ -170,7 +172,7 @@ gem "font-awesome-sass", "~> 5.13.0" # Parse CSS and add vendor prefixes to CSS rules using values from the Can # I Use website. (https://github.com/ai/autoprefixer-rails) -gem "autoprefixer-rails" +gem 'autoprefixer-rails' # Minimal embedded v8 for Ruby (https://github.com/discourse/mini_racer) # gem "mini_racer" @@ -180,28 +182,28 @@ gem "autoprefixer-rails" # ========= # # Provides binaries for WKHTMLTOPDF project in an easily accessible package. -gem "wkhtmltopdf-binary" +gem 'wkhtmltopdf-binary' # PDF generator (from HTML) gem for Ruby on Rails # (https://github.com/mileszs/wicked_pdf) -gem "wicked_pdf" +gem 'wicked_pdf' # This simple gem allows you to create MS Word docx documents from simple # html documents. This makes it easy to create dynamic reports and forms # that can be downloaded by your users as simple MS Word docx files. # (http://github.com/karnov/htmltoword) -gem "htmltoword" +gem 'htmltoword' # Filename sanitization for Ruby. This is useful when you generate filenames for # downloads from user input -gem "zaru" +gem 'zaru' # ==================== # # INTERNATIONALIZATION # # ==================== # -# gem "translation" -gem 'translation', git: "https://github.com/lagoan/translation_io_rails", branch: 'fix/broken_db_fake_method_calls' +gem 'translation', git: 'https://github.com/lagoan/translation_io_rails', + branch: 'fix/broken_db_fake_method_calls' # ========= # # UTILITIES # @@ -210,13 +212,15 @@ gem 'translation', git: "https://github.com/lagoan/translation_io_rails", branch # Run any code in parallel Processes(> use all CPUs) or Threads(> speedup # blocking operations). Best suited for map-reduce or e.g. parallel downloads/uploads. # TODO: Replace use of this with ActiveJob where possible -gem "parallel" +gem 'parallel' # Makes http fun again! Wrapper to simplify the native Net::HTTP libraries -gem "httparty" +gem 'httparty' # Autoload dotenv in Rails. (https://github.com/bkeepers/dotenv) -gem "dotenv-rails" +gem 'dotenv-rails' + +gem 'activerecord_json_validator' # ================================= # # ENVIRONMENT SPECIFIC DEPENDENCIES # @@ -224,152 +228,172 @@ gem "dotenv-rails" group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console - gem "byebug", platforms: %i[mri mingw x64_mingw] + gem 'byebug', platforms: %i[mri mingw x64_mingw] end -group :development, :test do +group :development, :test, :sandbox do # RSpec for Rails (https://github.com/rspec/rspec-rails) - gem "rspec-rails" + gem 'rspec-rails' # factory_bot_rails provides integration between factory_bot and rails 3 # or newer (http://github.com/thoughtbot/factory_bot_rails) - gem "factory_bot_rails" + gem 'factory_bot_rails' # Easily generate fake data (https://github.com/stympy/faker) - # gem "faker" + gem 'faker' # the instafailing RSpec progress bar formatter # (https://github.com/thekompanee/fuubar) - gem "fuubar" + gem 'fuubar' # Guard keeps an eye on your file modifications (http://guardgem.org) - gem "guard" + gem 'guard' # Guard gem for RSpec (https://github.com/guard/guard-rspec) - gem "guard-rspec" + gem 'guard-rspec' # Library for stubbing HTTP requests in Ruby. # (http://github.com/bblimke/webmock) - gem "webmock" + gem 'webmock' # Code coverage for Ruby 1.9+ with a powerful configuration library and # automatic merging of coverage across test suites # (http://github.com/colszowka/simplecov) - gem "simplecov", require: false + # gem 'simplecov', require: false # Strategies for cleaning databases. Can be used to ensure a clean state # for testing. (http://github.com/DatabaseCleaner/database_cleaner) - gem "database_cleaner", require: false + gem 'database_cleaner', require: false # Making tests easy on the fingers and eyes # (https://github.com/thoughtbot/shoulda) - gem "shoulda", require: false + gem 'shoulda', require: false # Mocking and stubbing library (http://gofreerange.com/mocha/docs) - gem "mocha", require: false + gem 'mocha', require: false # Adds support for Capybara system testing and selenium driver - gem "capybara" - gem "selenium-webdriver" + gem 'capybara' + gem 'selenium-webdriver' # Easy installation and use of web drivers to run system tests with browsers - gem "webdrivers" + gem 'webdrivers' # Automatically create snapshots when Cucumber steps fail with Capybara # and Rails (http://github.com/mattheworiordan/capybara-screenshot) - gem "capybara-screenshot" + gem 'capybara-screenshot' # Browser integration tests are expensive. We can mock external requests # in our tests, but once a browser is involved, we lose control. - gem "capybara-webmock" + gem 'capybara-webmock', '~> 0.6' # RSpec::CollectionMatchers lets you express expected outcomes on # collections of an object in an example. - gem "rspec-collection_matchers" + gem 'rspec-collection_matchers' # A set of RSpec matchers for testing Pundit authorisation policies. - gem "pundit-matchers" + gem 'pundit-matchers' # This gem brings back assigns to your controller tests as well as assert_template # to both controller and integration tests. - gem "rails-controller-testing" + gem 'rails-controller-testing' # Pull Request etiquette enforcement - gem "danger", '~> 8.4', require: false + gem 'danger', '~> 9.0', require: false end group :ci, :development do # Security vulnerability scanner for Ruby on Rails. # (http://brakemanscanner.org) - gem "brakeman" + gem 'brakeman' # Automatic Ruby code style checking tool. # (https://github.com/rubocop-hq/rubocop) # Rubocop style checks for DMP Roadmap projects. # (https://github.com/DMPRoadmap/rubocop-DMP_Roadmap) - gem "rubocop-dmp_roadmap" + # gem 'rubocop-dmp_roadmap' # Helper gem to require bundler-audit # (http://github.com/stewartmckee/bundle-audit) - gem "bundle-audit" + gem 'bundle-audit' + + # RuboCop is a Ruby code style checking and code formatting tool. It aims to enforce + # the community-driven Ruby Style Guide. + gem 'rubocop' + + # RuboCop rules for detecting and autocorrecting undecorated strings for i18n + # (gettext and rails-i18n) + gem 'rubocop-i18n' + + # A collection of RuboCop cops to check for performance optimizations in Ruby code. + gem 'rubocop-performance' + + # Automatic Rails code style checking tool. A RuboCop extension focused on enforcing + # Rails best practices and coding conventions. + gem 'rubocop-rails' + + # A RuboCop plugin for Rake tasks + gem 'rubocop-rake' + + # Code style checking for RSpec files. A plugin for the RuboCop code style enforcing + # & linting tool. + gem 'rubocop-rspec' + + # Thread-safety checks via static analysis. A plugin for the RuboCop code style + # enforcing & linting tool. + gem 'rubocop-thread_safety' end group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. - gem "listen" - gem "web-console" + gem 'listen' + gem 'web-console' # Spring speeds up development by keeping your application running in the background. # Read more: https://github.com/rails/spring - gem "spring" - gem "spring-watcher-listen" + gem 'spring' + gem 'spring-watcher-listen' # Simple Progress Bar for output to a terminal # (http://github.com/paul/progress_bar) - gem "progress_bar", require: false + gem 'progress_bar', require: false # A collection of text algorithms (http://github.com/threedaymonk/text) - gem "text", require: false + gem 'text', require: false # Better error page for Rails and other Rack apps # (https://github.com/charliesome/better_errors) - gem "better_errors" + gem 'better_errors' # Retrieve the binding of a method's caller. Can also retrieve bindings # even further up the stack. (http://github.com/banister/binding_of_caller) - gem "binding_of_caller" + gem 'binding_of_caller' # rspec command for spring # (https://github.com/jonleighton/spring-commands-rspec) - gem "spring-commands-rspec" + gem 'spring-commands-rspec' # Profiles loading speed for rack applications. (http://miniprofiler.com) - gem "rack-mini-profiler" + gem 'rack-mini-profiler' # Annotates Rails Models, routes, fixtures, and others based on the # database schema. (http://github.com/ctran/annotate_models) - gem "annotate" + gem 'annotate' # Add comments to your Gemfile with each dependency's description. # (https://github.com/ivantsepp/annotate_gem) - gem "annotate_gem" + gem 'annotate_gem' # help to kill N+1 queries and unused eager loading. # (https://github.com/flyerhzm/bullet) - gem "bullet" + gem 'bullet' # Documentation tool for consistent and usable documentation in Ruby. # (http://yardoc.org) - gem "yard" + gem 'yard' # TomDoc for YARD (http://rubyworks.github.com/yard-tomdoc) - gem "yard-tomdoc" -end + gem 'yard-tomdoc' -group :production, :staging, :sandbox do - gem 'syslog-logger' + group :production, :staging, :sandbox do + gem 'syslog-logger' + end end - -group :production, :staging, :sandbox, :development, :test do - # Temporarily move faker gem to all environments because it needs to be available in dev, production (for uat), staging and sandbox - # Changing to development, test, staging and sandbox only after finishing test on uat - gem "faker" -end \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 2dcca13d5f..fd6b397bb6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/lagoan/translation_io_rails - revision: d7917d9a8e84ea195259a11080c08d212b061839 + revision: 1975821394c8c84da9458d7c6a588c862cdcb46e branch: fix/broken_db_fake_method_calls specs: translation (1.22) @@ -43,6 +43,9 @@ GEM arel (>= 9.0) activerecord-nulldb-adapter (0.8.0) activerecord (>= 5.2.0, < 7.1) + activerecord_json_validator (2.1.3) + activerecord (>= 4.2.0, < 8) + json_schemer (~> 0.2.18) activestorage (5.2.8.1) actionpack (= 5.2.8.1) activerecord (= 5.2.8.1) @@ -72,11 +75,11 @@ GEM bindex (0.8.1) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) - bootsnap (1.15.0) + bootsnap (1.16.0) msgpack (~> 1.2) brakeman (5.4.0) builder (3.2.4) - bullet (7.0.4) + bullet (7.0.7) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) bundle-audit (0.1.0) @@ -97,7 +100,14 @@ GEM capybara-screenshot (1.0.26) capybara (>= 1.0, < 4) launchy - capybara-webmock (0.1.0) + capybara-webmock (0.6.0) + capybara (>= 2.4, < 4) + rack (>= 1.4) + rack-proxy (>= 0.6.0) + rexml (>= 3.2) + selenium-webdriver (~> 3.0) + webrick (>= 1.7) + childprocess (3.0.0) claide (1.1.0) claide-plugins (0.9.2) cork @@ -105,7 +115,7 @@ GEM open4 (~> 1.3) coderay (1.1.3) colored2 (3.1.2) - concurrent-ruby (1.1.10) + concurrent-ruby (1.2.0) contact_us (1.2.0) rails (>= 4.2.0) cork (0.3.0) @@ -113,18 +123,18 @@ GEM crack (0.4.5) rexml crass (1.0.6) - danger (8.6.1) + danger (9.2.0) claide (~> 1.0) claide-plugins (>= 0.9.2) colored2 (~> 3.1) cork (~> 0.1) - faraday (>= 0.9.0, < 2.0) + faraday (>= 0.9.0, < 3.0) faraday-http-cache (~> 2.0) git (~> 1.7) kramdown (~> 2.3) kramdown-parser-gfm (~> 1.0) no_proxy_fix - octokit (~> 4.7) + octokit (~> 5.0) terminal-table (>= 1, < 4) database_cleaner (2.0.1) database_cleaner-active_record (~> 2.0.0) @@ -132,7 +142,7 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.3.1) + date (3.3.3) debug_inspector (1.1.0) devise (4.8.1) bcrypt (~> 3.0) @@ -140,15 +150,10 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - devise_invitable (2.0.6) - devise_invitable (2.0.6) + devise_invitable (2.0.7) actionmailer (>= 5.0) devise (>= 4.6) diff-lcs (1.5.0) - docile (1.4.0) - dotenv (2.8.1) - dotenv-rails (2.8.1) - dotenv (= 2.8.1) dotenv (2.8.1) dotenv-rails (2.8.1) dotenv (= 2.8.1) @@ -160,44 +165,27 @@ GEM dragonfly-s3_data_store (1.3.0) dragonfly (~> 1.0) fog-aws - erubi (1.11.0) - excon (0.95.0) + ecma-re-validator (0.4.0) + regexp_parser (~> 2.2) + erubi (1.12.0) + excon (0.99.0) execjs (2.8.1) 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.0.0) + faker (3.1.1) i18n (>= 1.8.11, < 2) - faraday (1.10.2) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) + faraday (2.7.4) + faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) faraday-http-cache (2.4.1) faraday (>= 0.8) - faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.1) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) + faraday-net_http (3.0.2) ffi (1.15.5) flag_shih_tzu (0.3.23) - fog-aws (3.15.0) + fog-aws (3.18.0) fog-core (~> 2.1) fog-json (~> 1.1) fog-xml (~> 0.1) @@ -224,12 +212,10 @@ GEM locale (>= 2.0.5) prime text (>= 1.3.0) - git (1.12.0) + git (1.13.2) addressable (~> 2.8) rchardet (~> 1.8) - globalid (1.0.0) - activesupport (>= 5.0) - globalid (1.0.0) + globalid (1.1.0) activesupport (>= 5.0) guard (2.18.0) formatador (>= 0.2.4) @@ -245,39 +231,37 @@ GEM guard (~> 2.1) guard-compat (~> 1.1) rspec (>= 2.99.0, < 4.0) + hana (1.3.7) hashdiff (1.0.1) hashie (5.0.0) - highline (2.0.3) + highline (2.1.0) htmltoword (1.1.1) actionpack nokogiri rubyzip (>= 1.0) - httparty (0.20.0) - httparty (0.20.0) - mime-types (~> 3.0) + httparty (0.21.0) + mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - i18n (1.12.0) i18n (1.12.0) concurrent-ruby (~> 1.0) jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) json (2.6.3) - jwt (2.5.0) + json_schemer (0.2.24) + ecma-re-validator (~> 0.3) + hana (~> 1.3) + regexp_parser (~> 2.0) + uri_template (~> 0.7) + jwt (2.7.0) 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) - 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) - kaminari-core (= 1.2.2) kaminari-activerecord (1.2.2) activerecord kaminari-core (= 1.2.2) @@ -286,12 +270,11 @@ GEM rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - launchy (2.5.0) - addressable (~> 2.7) + launchy (2.5.2) + addressable (~> 2.8) ledermann-rails-settings (2.5.0) activerecord (>= 4.2) - listen (3.7.1) - listen (3.7.1) + listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) locale (2.1.3) @@ -299,7 +282,7 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.5.9) lumberjack (1.2.8) - mail (2.8.0) + mail (2.8.1) mini_mime (>= 0.1.1) net-imap net-pop @@ -307,27 +290,23 @@ GEM marcel (1.0.2) matrix (0.4.2) method_source (1.0.0) - mime-types (3.4.1) mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) - mime-types-data (3.2022.0105) mimemagic (0.3.10) nokogiri (~> 1) rake mini_mime (1.1.2) - mini_portile2 (2.8.0) - minitest (5.16.3) + minitest (5.17.0) mocha (2.0.2) ruby2_keywords (>= 0.0.5) msgpack (1.6.0) multi_json (1.15.0) multi_xml (0.6.0) - multipart-post (2.2.3) - mysql2 (0.5.4) + mysql2 (0.5.5) nap (1.1.0) nenv (0.3.0) - net-imap (0.3.2) + net-imap (0.3.4) date net-protocol net-pop (0.1.2) @@ -338,12 +317,9 @@ GEM net-protocol nio4r (2.5.8) no_proxy_fix (0.1.2) - nokogiri (1.13.10) - mini_portile2 (~> 2.8.0) - racc (~> 1.4) - nokogiri (1.13.10-arm64-darwin) + nokogiri (1.14.2-arm64-darwin) racc (~> 1.4) - nokogiri (1.13.10-x86_64-linux) + nokogiri (1.14.2-x86_64-linux) racc (~> 1.4) notiffany (0.1.3) nenv (~> 0.1) @@ -355,10 +331,10 @@ GEM rack (>= 1.2, < 4) snaky_hash (~> 2.0) version_gem (~> 1.1) - octokit (4.25.1) + octokit (5.6.1) faraday (>= 1, < 3) sawyer (~> 0.9) - omniauth (2.1.0) + omniauth (2.1.1) hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection @@ -377,7 +353,7 @@ GEM options (2.3.2) orm_adapter (0.5.0) parallel (1.22.1) - parser (3.1.3.0) + parser (3.2.1.0) ast (~> 2.4.1) pg (1.4.5) prime (0.1.2) @@ -386,30 +362,26 @@ GEM progress_bar (1.3.3) highline (>= 1.6, < 3) options (~> 2.3.0) - pry (0.14.1) + pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) public_suffix (5.0.1) - puma (6.0.0) + puma (6.1.0) nio4r (~> 2.0) - pundit (2.2.0) - pundit (2.2.0) + pundit (2.3.0) activesupport (>= 3.0.0) pundit-matchers (1.8.4) rspec-rails (>= 3.0.0) - racc (1.6.1) - rack (2.2.4) - rack-mini-profiler (3.0.0) + racc (1.6.2) + rack (2.2.6.2) rack-mini-profiler (3.0.0) rack (>= 1.2.0) - rack-protection (3.0.4) + rack-protection (3.0.5) rack - rack-proxy (0.7.4) + rack-proxy (0.7.6) rack rack-test (2.0.2) rack (>= 1.3) - rack-test (2.0.2) - rack (>= 1.3) rails (5.2.8.1) actioncable (= 5.2.8.1) actionmailer (= 5.2.8.1) @@ -430,7 +402,7 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.4) + rails-html-sanitizer (1.5.0) loofah (~> 2.19, >= 2.19.1) railties (5.2.8.1) actionpack (= 5.2.8.1) @@ -441,30 +413,29 @@ GEM rainbow (3.1.1) rake (13.0.6) rb-fsevent (0.11.2) - rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) rchardet (1.8.0) recaptcha (5.12.3) json - regexp_parser (2.6.1) - responders (3.0.1) - actionpack (>= 5.0) - railties (>= 5.0) + regexp_parser (2.7.0) + responders (3.1.0) + actionpack (>= 5.2) + railties (>= 5.2) rexml (3.2.5) - rollbar (3.3.2) + rollbar (3.4.0) rspec (3.12.0) rspec-core (~> 3.12.0) rspec-expectations (~> 3.12.0) rspec-mocks (~> 3.12.0) rspec-collection_matchers (1.2.0) rspec-expectations (>= 2.99.0.beta1) - rspec-core (3.12.0) + rspec-core (3.12.1) rspec-support (~> 3.12.0) - rspec-expectations (3.12.0) + rspec-expectations (3.12.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-mocks (3.12.1) + rspec-mocks (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-rails (5.1.2) @@ -476,43 +447,36 @@ GEM rspec-mocks (~> 3.10) rspec-support (~> 3.10) rspec-support (3.12.0) - rubocop (1.40.0) + rubocop (1.45.1) json (~> 2.3) parallel (~> 1.10) - parser (>= 3.1.2.1) + parser (>= 3.2.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.23.0, < 2.0) + rubocop-ast (>= 1.24.1, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.24.0) - parser (>= 3.1.1.0) - rubocop-dmp_roadmap (1.1.2) - rubocop (>= 0.58.2) - rubocop-rails_config (>= 0.2.2) - rubocop-rspec (>= 1.27.0) - rubocop-minitest (0.25.0) - rubocop (>= 0.90, < 2.0) - rubocop-packaging (0.5.2) - rubocop (>= 1.33, < 2.0) - rubocop-performance (1.15.1) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.26.0) + parser (>= 3.2.1.0) + rubocop-capybara (2.17.1) + rubocop (~> 1.41) + rubocop-i18n (3.0.0) + rubocop (~> 1.0) + rubocop-performance (1.16.0) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) - rubocop-rails (2.17.3) + rubocop-rails (2.17.4) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) - rubocop-rails_config (1.12.0) - railties (>= 5.0) - rubocop (>= 1.39.0) - rubocop-ast (>= 1.0.1) - rubocop-minitest (~> 0.22) - rubocop-packaging (~> 0.5) - rubocop-performance (~> 1.11) - rubocop-rails (~> 2.0) - rubocop-rspec (2.16.0) + rubocop-rake (0.6.0) + rubocop (~> 1.0) + rubocop-rspec (2.18.1) rubocop (~> 1.33) + rubocop-capybara (~> 2.17) + rubocop-thread_safety (0.4.4) + rubocop (>= 0.53.0) ruby-progressbar (1.11.0) ruby2_keywords (0.0.5) ruby_dig (0.0.2) @@ -530,10 +494,9 @@ GEM sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) - selenium-webdriver (4.7.1) - rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) - websocket (~> 1.0) + selenium-webdriver (3.142.7) + childprocess (>= 0.5, < 4.0) + rubyzip (>= 1.2.2) semantic_range (3.0.0) shellany (0.0.1) shoulda (4.0.0) @@ -542,25 +505,19 @@ GEM shoulda-context (2.0.0) shoulda-matchers (4.5.1) activesupport (>= 4.2.0) - simplecov (0.21.2) - docile (~> 1.1) - simplecov-html (~> 0.11) - simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) - simplecov_json_formatter (0.1.4) singleton (0.1.1) snaky_hash (2.0.1) hashie version_gem (~> 1.1, >= 1.1.1) - spring (4.1.0) + spring (4.1.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.1.1) + sprockets (4.2.0) concurrent-ruby (~> 1.0) - rack (> 1, < 3) + rack (>= 2.2.4, < 4) sprockets-rails (3.4.2) actionpack (>= 5.2) activesupport (>= 5.2) @@ -570,19 +527,18 @@ GEM unicode-display_width (>= 1.1.1, < 3) text (1.3.1) thor (1.2.1) - thor (1.2.1) thread_safe (0.3.6) tilt (2.0.11) - timeout (0.3.1) + timeout (0.3.2) tomparse (0.4.2) turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) - tzinfo (1.2.10) - tzinfo (1.2.10) + tzinfo (1.2.11) thread_safe (~> 0.1) - unicode-display_width (2.3.0) + unicode-display_width (2.4.2) uniform_notifier (1.16.0) + uri_template (0.7.0) version_gem (1.1.1) warden (1.2.9) rack (>= 2.0.9) @@ -591,21 +547,20 @@ GEM activemodel (>= 5.0) bindex (>= 0.4.0) railties (>= 5.0) - webdrivers (5.2.0) + webdrivers (4.7.0) nokogiri (~> 1.6) rubyzip (>= 1.3.0) - selenium-webdriver (~> 4.0) + selenium-webdriver (> 3.141, < 5.0) webmock (3.18.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webpacker (5.4.3) + webpacker (5.4.4) activesupport (>= 5.2) rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) webrick (1.7.0) - websocket (1.2.9) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -616,8 +571,6 @@ GEM nokogiri (~> 1.8) yard (0.9.28) webrick (~> 1.7.0) - yard (0.9.28) - webrick (~> 1.7.0) yard-tomdoc (0.7.1) tomparse (>= 0.4.0) yard @@ -625,11 +578,11 @@ GEM PLATFORMS arm64-darwin-22 - ruby x86_64-linux DEPENDENCIES activerecord-nulldb-adapter + activerecord_json_validator annotate annotate_gem api-pagination @@ -643,9 +596,9 @@ DEPENDENCIES byebug capybara capybara-screenshot - capybara-webmock + capybara-webmock (~> 0.6) contact_us - danger (~> 8.4) + danger (~> 9.0) database_cleaner devise devise_invitable @@ -686,12 +639,17 @@ DEPENDENCIES rollbar rspec-collection_matchers rspec-rails - rubocop-dmp_roadmap + rubocop + rubocop-i18n + rubocop-performance + rubocop-rails + rubocop-rake + rubocop-rspec + rubocop-thread_safety sass-rails sassc-rails selenium-webdriver shoulda - simplecov spring spring-commands-rspec spring-watcher-listen diff --git a/README.md b/README.md index 43b4b99042..a4801d2b53 100644 --- a/README.md +++ b/README.md @@ -91,4 +91,4 @@ See the [Contribution Guide](https://github.com/portagenetwork/roadmap/blob/inte
-[![Alliance](Alliance_logo.jpg)](https://alliancecan.ca) \ No newline at end of file +[![Alliance](Alliance_logo.jpg)](https://alliancecan.ca) diff --git a/README.rdoc b/README.rdoc deleted file mode 100644 index 8f67343403..0000000000 --- a/README.rdoc +++ /dev/null @@ -1,36 +0,0 @@ -= DMPRoadmap - -Roadmap is a data management planning tool, available at https://github.com/DMPRoadmap/roadmap - -Development of the Roadmap is provided by the Digital Curation Centre and the University of California Curation Center. - -The tool has four main functions -1. To help create and maintain different versions of Data Management Plans; -2. To provide useful guidance on data management issues and how to meet research funders' requirements; -3. To export attractive and useful plans in a variety of formats; -4. To allow collaborative work when creating Data Management Plans. - -== Documentation & Support - -* You can contact us by email, roadmap-l@listserv.ucop.edu, but we can only provide limited support for your installation - -== Bugs & Feature Requests - -* Bug Reports & Feature Requests: https://github.com/DMPRoadmap/roadmap/issues -* Please prefix your request with either: 'Bug:' or 'Feature:' - -== Prerequisites - -Roadmap is a Ruby on Rails application and you will need to have Ruby 2.0.0p247 or greater installed on your server and a MySQL server v5.0 or greater. - -Further details on how to install Ruby on Rails applications are available from the Ruby on Rails site, http://rubyonrails.org - -You may also find the following resources handy: - -* The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html -* Ruby on Rails Tutorial Book: http://www.railstutorial.org/ - - -== Copyright - -The Roadmap project uses an MIT License. The full text of the license can be found at: https://github.com/DMPRoadmap/roadmap/blob/master/LICENSE.md diff --git a/Rakefile b/Rakefile index 700a8a33cf..42f9525d64 100755 --- a/Rakefile +++ b/Rakefile @@ -10,7 +10,7 @@ # task default: :test -require_relative "config/application" +require_relative 'config/application' DMPRoadmap::Application.load_tasks diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico index 40ade36c75..4ddb00d5f7 100644 Binary files a/app/assets/images/favicon.ico and b/app/assets/images/favicon.ico differ diff --git a/app/assets/images/logo.png b/app/assets/images/logo.png new file mode 100644 index 0000000000..e4cc2dcfcd Binary files /dev/null and b/app/assets/images/logo.png differ diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 35268bdad4..4e5640a461 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -11,7 +11,7 @@ // Pull in the webapcker managed copy of JQuery-UI Stylesheets // we are using the Datepicker and Sortable at the very least -// @import "../../../node_modules/jquery-ui-sass/assets/sass/jquery-ui.scss"; +@import "../../../node_modules/jquery-ui-sass/assets/sass/jquery-ui.scss"; @import "font-awesome-sprockets"; @import "font-awesome"; diff --git a/app/assets/stylesheets/blocks/_modal_search.scss b/app/assets/stylesheets/blocks/_modal_search.scss new file mode 100644 index 0000000000..75e87c828e --- /dev/null +++ b/app/assets/stylesheets/blocks/_modal_search.scss @@ -0,0 +1,75 @@ +.modal-search-block { + border: 1px solid $color-grey; + margin-bottom: 10px; + padding: 10px 5px; +} + +.modal-search .modal-dialog { + /* Make the dialog 80% of the screen height/width */ + width: 80%; + // height: 80%; +} +.modal-search .modal-body { + /* 100% = dialog height, 50px = header (27.5px) + footer (21px) */ + // max-height: calc(80% - 50px); + max-height: 450px; + overflow-y: scroll; +} + +.modal-search-results-pagination { + margin-bottom: 10px; +} + +.modal-search-result { + margin-top: 5px; + padding-bottom: 5px; + + .modal-search-result-label { + font-size: 1.6rem; + font-weight: 500; + } + + .tags > .tag { + display: inline-block; + margin: 5px 2px; + } + .tags .facet { + border: 1px solid $color-blue; + border-radius: 25px; + padding: 2px 5px; + } + + div { + margin-bottom: 5px; + } + + dl { + margin-left: 20px; + + dd { + margin-bottom: 5px; + } + } +} + +.modal-search-results .modal-search-result { + border-bottom: 1px solid $color-grey; +} + +/* the 'Select' button displayed in the modal dialog */ +.modal-search-result .modal-search-result-selector, +.modal-search-result .modal-search-result-unselector { + display: inline-block; + background-color: $color-white; + border-radius: 25px; + padding: 2px 5px; + font-size: 1.3rem; +} +.modal-search-result .modal-search-result-selector { + background-color: $color-green; + color: $color-white; +} +.modal-search-result .modal-search-result-unselector { + border: 1px solid $color-red; + color: $color-red; +} diff --git a/app/assets/stylesheets/blocks/_question_form.scss b/app/assets/stylesheets/blocks/_question_form.scss new file mode 100644 index 0000000000..165223aca6 --- /dev/null +++ b/app/assets/stylesheets/blocks/_question_form.scss @@ -0,0 +1,35 @@ +.question-body { + display: flex; + padding: 15px 0; + .question-section { + flex: 8; + position: relative; + .toggle-guidance-section { + position: absolute; + top: 0; + right: 0; + background-color: $color-portage-blue; + color: $color-white; + padding: 10px 5px; + cursor: pointer; + + text-orientation: mixed; + writing-mode: vertical-rl; + &.disabled { + background-color: $color-muted; + cursor: not-allowed; + } + } + .question-form { + padding-right: 50px; + padding-top: 10px; + } + } + + .guidance-section { + flex: 4; + border-left: 5px solid $color-portage-blue; + padding-left: 5px; + } + +} \ No newline at end of file diff --git a/app/assets/stylesheets/variables/_colours.scss b/app/assets/stylesheets/variables/_colours.scss index 5ee371630d..d3e9bdec71 100644 --- a/app/assets/stylesheets/variables/_colours.scss +++ b/app/assets/stylesheets/variables/_colours.scss @@ -5,6 +5,7 @@ $color-black: #000; $color-white: #FFF; $color-red: #b94a48; +$color-green: #4c8d3f; $color-grey: #4F5253; $color-grey-darkest: #222; $color-grey-darker: #333; diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb index fa6ee697da..9aec230539 100644 --- a/app/channels/application_cable/channel.rb +++ b/app/channels/application_cable/channel.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true module ApplicationCable - class Channel < ActionCable::Channel::Base - end - end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index b08f85080a..8d6c2a1bf4 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true module ApplicationCable - class Connection < ActionCable::Connection::Base - end - end diff --git a/app/controllers/answers_controller.rb b/app/controllers/answers_controller.rb index 48980290bf..83ef83fe6e 100644 --- a/app/controllers/answers_controller.rb +++ b/app/controllers/answers_controller.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true -class AnswersController < ApplicationController +# 3.1.0: all changes here are 3.1.0 updates. Keep them +# Controller that handles Answers to DMP questions +class AnswersController < ApplicationController respond_to :html include ConditionsHelper @@ -12,6 +14,7 @@ class AnswersController < ApplicationController # `remote: true` in the
tag and just send back the ERB. # Consider using ActionCable for the progress bar(s) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def create_or_update p_params = permitted_params @@ -21,21 +24,17 @@ def create_or_update unless p.question_exists?(p_params[:question_id]) # rubocop:disable Layout/LineLength render(status: :not_found, json: { - msg: _("There is no question with id %{question_id} associated to plan id %{plan_id} for which to create or update an answer") % { - question_id: p_params[:question_id], - plan_id: p_params[:plan_id] - } + msg: format(_('There is no question with id %{question_id} associated to plan id %{plan_id} for which to create or update an answer'), question_id: p_params[:question_id], plan_id: p_params[:plan_id]) }) # rubocop:enable Layout/LineLength return end rescue ActiveRecord::RecordNotFound + # rubocop:disable Layout/LineLength render(status: :not_found, json: { - msg: _("There is no plan with id %{id} for which to create or update an answer") % { - id: p_params[:plan_id] - } + msg: format(_('There is no plan with id %{id} for which to create or update an answer'), id: p_params[:plan_id]) }) - + # rubocop:enable Layout/LineLength return end q = Question.find(p_params[:question_id]) @@ -127,19 +126,19 @@ def create_or_update send_webhooks(current_user, @answer) render json: { - "qn_data": qn_data, - "section_data": section_data, - "question" => { - "id" => @question.id, - "answer_lock_version" => @answer.lock_version, - "locking" => if @stale_answer - render_to_string(partial: "answers/locking", locals: { + qn_data: qn_data, + section_data: section_data, + 'question' => { + 'id' => @question.id, + 'answer_lock_version' => @answer.lock_version, + 'locking' => if @stale_answer + render_to_string(partial: 'answers/locking', locals: { question: @question, answer: @stale_answer, user: @answer.user }, formats: [:html]) end, - "form" => render_to_string(partial: "answers/new_edit", locals: { + 'form' => render_to_string(partial: 'answers/new_edit', locals: { template: template, question: @question, answer: @answer, @@ -147,13 +146,13 @@ def create_or_update locking: false, base_template_org: template.base_org }, formats: [:html]), - "answer_status" => render_to_string(partial: "answers/status", locals: { + 'answer_status' => render_to_string(partial: 'answers/status', locals: { answer: @answer }, formats: [:html]) }, - "plan" => { - "id" => @plan.id, - "progress" => render_to_string(partial: "plans/progress", locals: { + 'plan' => { + 'id' => @plan.id, + 'progress' => render_to_string(partial: 'plans/progress', locals: { plan: @plan, current_phase: @section.phase }, formats: [:html]) @@ -164,10 +163,11 @@ def create_or_update # rubocop:enable Style/GuardClause end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity private + # rubocop:disable Metrics/AbcSize def permitted_params permitted = params.require(:answer) .permit(:id, :text, :plan_id, :user_id, :question_id, @@ -184,11 +184,11 @@ def permitted_params permitted[:question_option_ids] = [] if params[:answer][:question_option_ids].nil? permitted end + # rubocop:enable Metrics/AbcSize def check_answered(section, q_array, all_answers) n_qs = section.questions.select { |question| q_array.include?(question.id) }.length n_ans = all_answers.select { |ans| q_array.include?(ans.question.id) and ans.answered? }.length [n_qs, n_ans] end - end diff --git a/app/controllers/api/v0/base_controller.rb b/app/controllers/api/v0/base_controller.rb index f90f9c7320..32502cafda 100644 --- a/app/controllers/api/v0/base_controller.rb +++ b/app/controllers/api/v0/base_controller.rb @@ -1,127 +1,134 @@ # frozen_string_literal: true -class Api::V0::BaseController < ApplicationController - - protect_from_forgery with: :null_session - before_action :define_resource, only: %i[destroy show update] - respond_to :json +module Api + module V0 + # Generic controller for API V0 + class BaseController < ApplicationController + protect_from_forgery with: :null_session + before_action :define_resource, only: %i[destroy show update] + respond_to :json + + # POST /api/{plural_resource_name} + def create + define_resource(resource_class.new(resource_params)) + + if retrieve_resource.save + render :show, status: :created + else + render json: retrieve_resource.errors, status: :unprocessable_entity + end + end - # POST /api/{plural_resource_name} - def create - define_resource(resource_class.new(resource_params)) + # DELETE /api/{plural_resource_name}/1 + def destroy + retrieve_resource.destroy + head :no_content + end - if retrieve_resource.save - render :show, status: :created - else - render json: retrieve_resource.errors, status: :unprocessable_entity - end - end + # GET /api/{plural_resource_name} + def index + plural_resource_name = "@#{resource_name.pluralize}" + resources = resource_class.where(query_params) + .page(page_params[:page]) + .per(page_params[:page_size]) - # DELETE /api/{plural_resource_name}/1 - def destroy - retrieve_resource.destroy - head :no_content - end + instance_variable_set(plural_resource_name, resources) + respond_with instance_variable_get(plural_resource_name) + end - # GET /api/{plural_resource_name} - def index - plural_resource_name = "@#{resource_name.pluralize}" - resources = resource_class.where(query_params) - .page(page_params[:page]) - .per(page_params[:page_size]) + # GET /api/{plural_resource_name}/1 + def show + respond_with retrieve_resource + end - instance_variable_set(plural_resource_name, resources) - respond_with instance_variable_get(plural_resource_name) - end + # PATCH/PUT /api/{plural_resource_name}/1 + def update + if retrieve_resource.update(resource_params) + render :show + else + render json: retrieve_resource.errors, status: :unprocessable_entity + end + end - # GET /api/{plural_resource_name}/1 - def show - respond_with retrieve_resource - end + private - # PATCH/PUT /api/{plural_resource_name}/1 - def update - if retrieve_resource.update(resource_params) - render :show - else - render json: retrieve_resource.errors, status: :unprocessable_entity - end - end + # The resource from the created instance variable + # + # Returns Object + def retrieve_resource + instance_variable_get("@#{resource_name}") + end - private + # The allowed parameters for searching. Override this method in each API + # controller to permit additional parameters to search on + # + # Returns Hash + def query_params + {} + end - # The resource from the created instance variable - # - # Returns Object - def retrieve_resource - instance_variable_get("@#{resource_name}") - end + # The allowed parameters for pagination + # + # Returns Hash + def page_params + params.permit(:page, :page_size) + end - # The allowed parameters for searching. Override this method in each API - # controller to permit additional parameters to search on - # - # Returns Hash - def query_params - {} - end + def plan_params + params.permit(:template_id, :plan[:title], :plan[:email]) + end - # The allowed parameters for pagination - # - # Returns Hash - def page_params - params.permit(:page, :page_size) - end + # The resource class based on the controller + # + # Returns Object + def resource_class + @resource_class ||= resource_name.classify.constantize + end - # The resource class based on the controller - # - # Returns Object - def resource_class - @resource_class ||= resource_name.classify.constantize - end + # The singular name for the resource class based on the controller + # + # Returns String + def resource_name + @resource_name ||= controller_name.singularize + end - # The singular name for the resource class based on the controller - # - # Returns String - def resource_name - @resource_name ||= controller_name.singularize - end + # Only allow a trusted parameter "white list" through. + # If a single resource is loaded for #create or #update, + # then the controller for the resource must implement + # the method "#{resource_name}_params" to limit permitted + # parameters for the individual model. + def resource_params + @resource_params ||= send("#{resource_name}_params") + end - # Only allow a trusted parameter "white list" through. - # If a single resource is loaded for #create or #update, - # then the controller for the resource must implement - # the method "#{resource_name}_params" to limit permitted - # parameters for the individual model. - def resource_params - @resource_params ||= send("#{resource_name}_params") - end + # Use callbacks to share common setup or constraints between actions. + def define_resource(resource = nil) + resource ||= resource_class.find(params[:id]) + instance_variable_set("@#{resource_name}", resource) + end - # Use callbacks to share common setup or constraints between actions. - def define_resource(resource = nil) - resource ||= resource_class.find(params[:id]) - instance_variable_set("@#{resource_name}", resource) - end + def authenticate + authenticate_token || render_bad_credentials + end - def authenticate - authenticate_token || render_bad_credentials - end + def authenticate_token + authenticate_with_http_token do |token, _options| + # reject the empty string as it is our base empty token + if token == '' + false + else + @token = token + @user = User.find_by(api_token: token) + # if no user found, return false, otherwise true + !@user.nil? && @user.can_use_api? + end + end + end - def authenticate_token - authenticate_with_http_token do |token, _options| - # reject the empty string as it is our base empty token - if token != "" - @token = token - @user = User.find_by(api_token: token) - # if no user found, return false, otherwise true - !@user.nil? && @user.can_use_api? - else - false + def render_bad_credentials + headers['WWW-Authenticate'] = 'Token realm=""' + render json: _('Bad Credentials'), status: 401 end end end - - def render_bad_credentials - headers["WWW-Authenticate"] = "Token realm=\"\"" - render json: _("Bad Credentials"), status: 401 - end - end diff --git a/app/controllers/api/v0/departments_controller.rb b/app/controllers/api/v0/departments_controller.rb index 5bd31d6f18..e573522b78 100644 --- a/app/controllers/api/v0/departments_controller.rb +++ b/app/controllers/api/v0/departments_controller.rb @@ -1,80 +1,87 @@ # frozen_string_literal: true -class Api::V0::DepartmentsController < Api::V0::BaseController - - before_action :authenticate - - ## - # Create a new department based on the information passed in JSON to the API - def create - raise Pundit::NotAuthorizedError unless Api::V0::DepartmentsPolicy.new(@user, nil).index? - - @department = Department.new(org: @user.org, - code: params[:code], - name: params[:name]) - if @department.save - redirect_to api_v0_departments_path - else - # the department did not save - headers["WWW-Authenticate"] = "Token realm=\"\"" - render json: _("Departments code and name must be unique"), status: 400 - end - end +module Api + module V0 + # Handles CRUD operations for Departments in API V0 + class DepartmentsController < Api::V0::BaseController + before_action :authenticate + + ## + # Create a new department based on the information passed in JSON to the API + def create + raise Pundit::NotAuthorizedError unless Api::V0::DepartmentsPolicy.new(@user, nil).index? + + @department = Department.new(org: @user.org, + code: params[:code], + name: params[:name]) + if @department.save + redirect_to api_v0_departments_path + else + # the department did not save + headers['WWW-Authenticate'] = 'Token realm=""' + render json: _('Departments code and name must be unique'), status: 400 + end + end - ## - # Lists the departments for the API user's organisation - def index - raise Pundit::NotAuthorizedError unless Api::V0::DepartmentsPolicy.new(@user, nil).index? + ## + # Lists the departments for the API user's organisation + def index + raise Pundit::NotAuthorizedError unless Api::V0::DepartmentsPolicy.new(@user, nil).index? - @departments = @user.org.departments - end + @departments = @user.org.departments + end - ## - # List the users for each department on the organisation - def users - raise Pundit::NotAuthorizedError unless Api::V0::DepartmentsPolicy.new(@user, nil).users? + ## + # List the users for each department on the organisation + def users + raise Pundit::NotAuthorizedError unless Api::V0::DepartmentsPolicy.new(@user, nil).users? - @users = @user.org.users.includes(:department) - end + @users = @user.org.users.includes(:department) + end - ## - # Assign the list of users to the passed department id - def assign_users - @department = Department.find(params[:id]) + ## + # Assign the list of users to the passed department id + def assign_users + @department = Department.find(params[:id]) - unless Api::V0::DepartmentsPolicy.new(@user, @department).assign_users? - raise Pundit::NotAuthorizedError - end + raise Pundit::NotAuthorizedError unless Api::V0::DepartmentsPolicy.new(@user, @department).assign_users? - assign_users_to(@department.id) - redirect_to users_api_v0_departments_path - end + assign_users_to(@department.id) - ## - # Remove departments from the list of users - def unassign_users - unless Api::V0::DepartmentsPolicy.new(@user, @department).assign_users? - raise Pudndit::NotAuthorizedError - end + # Added "status: :see_other" to redirect_to (as we require rediect to be a GET). + # See https://makandracards.com/makandra/38347-redirecting-responses-for-patch-or-delete-will-not-redirect-with-get + redirect_to users_api_v0_departments_path, status: :see_other + end - assign_users_to(nil) - redirect_to users_api_v0_departments_path - end + ## + # Remove departments from the list of users + def unassign_users + @department = Department.find(params[:id]) + + raise Pundit::NotAuthorizedError unless Api::V0::DepartmentsPolicy.new(@user, @department).assign_users? - private + assign_users_to(nil) - def assign_users_to(department_id) - params[:users].each do |email| - reassign = User.find_by(email: email) - # Currently the validation is that the user's org matches the API user's - # Not sure if this is possible to capture in pundit - unless @user.present? && @user.org == reassign&.org - raise Pundit::NotAuthorizedError, _("user #{email} was not found on your organisation") + # Added "status: :see_other" to redirect_to (as we require rediect to be a GET). + # See https://makandracards.com/makandra/38347-redirecting-responses-for-patch-or-delete-will-not-redirect-with-get + redirect_to users_api_v0_departments_path, status: :see_other end - reassign.department_id = department_id - reassign.save! + private + + def assign_users_to(department_id) + params[:users].each do |email| + reassign = User.find_by(email: email) + # Currently the validation is that the user's org matches the API user's + # Not sure if this is possible to capture in pundit + unless @user.present? && @user.org == reassign&.org + raise Pundit::NotAuthorizedError, _("user #{email} was not found on your organisation") + end + + reassign.department_id = department_id + reassign.save! + end + end end end - end diff --git a/app/controllers/api/v0/guidance_groups_controller.rb b/app/controllers/api/v0/guidance_groups_controller.rb index 17beae33d4..7c828f1f69 100644 --- a/app/controllers/api/v0/guidance_groups_controller.rb +++ b/app/controllers/api/v0/guidance_groups_controller.rb @@ -1,26 +1,27 @@ # frozen_string_literal: true -class Api::V0::GuidanceGroupsController < Api::V0::BaseController +module Api + module V0 + # Handles GuidanceGroup queries for API V0 + class GuidanceGroupsController < Api::V0::BaseController + before_action :authenticate - before_action :authenticate + def index + raise Pundit::NotAuthorizedError unless Api::V0::GuidanceGroupPolicy.new(@user, :guidance_group).index? - def index - unless Api::V0::GuidanceGroupPolicy.new(@user, :guidance_group).index? - raise Pundit::NotAuthorizedError - end - - @all_viewable_groups = GuidanceGroup.all_viewable(@user) - respond_with @all_viewable_groups - end + @all_viewable_groups = GuidanceGroup.all_viewable(@user) + respond_with @all_viewable_groups + end - def pundit_user - @user - end + def pundit_user + @user + end - private + private - def query_params - params.permit(:id) + def query_params + params.permit(:id) + end + end end - end diff --git a/app/controllers/api/v0/plans_controller.rb b/app/controllers/api/v0/plans_controller.rb index 5e30069bda..8a88f8aff0 100644 --- a/app/controllers/api/v0/plans_controller.rb +++ b/app/controllers/api/v0/plans_controller.rb @@ -1,123 +1,126 @@ # frozen_string_literal: true -class Api::V0::PlansController < Api::V0::BaseController - - include Paginable - - before_action :authenticate - - ## - # Creates a new plan based on the information passed in JSON to the API - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - def create - @template = Template.live(params[:template_id]) - raise Pundit::NotAuthorizedError unless Api::V0::PlansPolicy.new(@user, @template).create? - - plan_user = User.find_by(email: params[:plan][:email]) - # ensure user exists - if plan_user.blank? - User.invite!({ email: params[:plan][:email] }, @user) - plan_user = User.find_by(email: params[:plan][:email]) - plan_user.org = @user.org - plan_user.save - end - # ensure user's organisation is the same as api user's - unless plan_user.org == @user.org - raise Pundit::NotAuthorizedError, _("user must be in your organisation") - end - - # initialize the plan - @plan = Plan.new - - # Attach the user as the PI and Data Contact - @plan.contributors << Contributor.new( - name: [plan_user.firstname, plan_user.surname].join(" "), - email: plan_user.email, - investigation: true, - data_curation: true - ) - - # set funder name to template's org, or original template's org - @plan.funder_id = if @template.customization_of.nil? - @template.org.id - else - Template.where( - family_id: @template.customization_of - ).first.org.id - end - @plan.template = @template - @plan.title = params[:plan][:title] - if @plan.save - @plan.assign_creator(plan_user) - respond_with @plan - else - # the plan did not save - headers["WWW-Authenticate"] = "Token realm=\"\"" - render json: _("Bad Parameters"), status: 400 +module Api + module V0 + # Primary controller for API V0 that handles CRUD operations for Plans + class PlansController < Api::V0::BaseController + include Paginable + + before_action :authenticate + # before_action :page_params, except: %i[heartbeat] + + ## + # Creates a new plan based on the information passed in JSON to the API + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def create + @template = Template.live(params[:template_id]) + raise Pundit::NotAuthorizedError unless Api::V0::PlansPolicy.new(@user, @template).create? + + plan_user = User.find_by(email: params[:plan][:email]) + # ensure user exists + if plan_user.blank? + User.invite!({ email: params[:plan][:email] }, @user) + plan_user = User.find_by(email: params[:plan][:email]) + plan_user.org = @user.org + plan_user.save + end + # ensure user's organisation is the same as api user's + raise Pundit::NotAuthorizedError, _('user must be in your organisation') unless plan_user.org == @user.org + + # initialize the plan + @plan = Plan.new + @plan.org = @user.org + + # Attach the user as the PI and Data Contact + @plan.contributors << Contributor.new( + name: [plan_user.firstname, plan_user.surname].join(' '), + email: plan_user.email, + investigation: true, + data_curation: true + ) + + # set funder name to template's org, or original template's org + @plan.funder_id = if @template.customization_of.nil? + @template.org.id + else + Template.where( + family_id: @template.customization_of + ).first.org.id + end + @plan.template = @template + @plan.title = params[:plan][:title] + + if @plan.save + @plan.add_user!(plan_user.id, :creator) + respond_with @plan + else + # the plan did not save + headers['WWW-Authenticate'] = 'Token realm=""' + render json: _('Bad Parameters'), status: 400 + end + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def index + raise Pundit::NotAuthorizedError unless Api::V0::PlansPolicy.new(@user, nil).index? + + if params[:per_page].present? + max_pages = Rails.configuration.x.application.api_max_page_size + params[:per_page] = max_pages if params[:per_page].to_i > max_pages + end + + # Get all the Org Admin plans + org_admin_plans = @user.org.org_admin_plans + @plans = org_admin_plans.includes([{ roles: :user }, { answers: :question_options }, + template: [{ phases: { + sections: { questions: %i[question_format themes] } + } }, :org]]) + + # Filter on list of users + user_ids = extract_param_list(params, 'user') + @plans = @plans.where(roles: { user_id: user_ids, access: Role.bit_values(:editor) }) if user_ids.present? + # filter on dates + if params['created_after'].present? || params['created_before'].present? + @plans = @plans.where(created_at: dates_to_range(params, 'created_after', 'created_before')) + end + if params['updated_after'].present? || params['updated_before'].present? + @plans = @plans.where(updated_at: dates_to_range(params, 'updated_after', 'updated_before')) + end + if params['remove_tests'].present? && params['remove_tests'].downcase == 'true' + @plans = @plans.where.not(visibility: Plan.visibilities[:is_test]) + end + # filter on funder (dmptemplate_id) + template_ids = extract_param_list(params, 'template') + @plans = @plans.where(templates: { family_id: template_ids }) if template_ids.present? + # filter on id(s) + plan_ids = extract_param_list(params, 'plan') + @plans = @plans.where(id: plan_ids) if plan_ids.present? + # apply pagination after filtering + @args = { per_page: params[:per_page], page: params[:page] } + @plans = refine_query(@plans) + respond_with @plans + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + + private + + def extract_param_list(params, attribute) + list = params.fetch("#{attribute}[]", []) + val = params.fetch(attribute, []) + list << val if val.present? + list + end + + # takes in the params hash and converts to a date-range + def dates_to_range(hash, start, stop) + today = Date.today + start_date = Date.parse(hash.fetch(start, today.prev_month.to_date.to_s)) + end_date = Date.parse(hash.fetch(stop, today.to_date.to_s)) + 1.day + start_date..end_date + end end end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - def index - raise Pundit::NotAuthorizedError unless Api::V0::PlansPolicy.new(@user, nil).index? - - if params[:per_page].present? - max_pages = Rails.configuration.x.application.api_max_page_size - params[:per_page] = max_pages if params[:per_page].to_i > max_pages - end - - # Get all the Org Admin plans - org_admin_plans = @user.org.org_admin_plans - @plans = org_admin_plans.includes([{ roles: :user }, { answers: :question_options }, - template: [{ phases: { - sections: { questions: %i[question_format themes] } - } }, :org]]) - - # Filter on list of users - user_ids = extract_param_list(params, "user") - if user_ids.present? - @plans = @plans.where(roles: { user_id: user_ids, access: Role.bit_values(:editor) }) - end - # filter on dates - if params["created_after"].present? || params["created_before"].present? - @plans = @plans.where(created_at: dates_to_range(params, "created_after", "created_before")) - end - if params["updated_after"].present? || params["updated_before"].present? - @plans = @plans.where(updated_at: dates_to_range(params, "updated_after", "updated_before")) - end - if params["remove_tests"].present? && params["remove_tests"].downcase == "true" - @plans = @plans.where.not(visibility: Plan.visibilities[:is_test]) - end - # filter on funder (dmptemplate_id) - template_ids = extract_param_list(params, "template") - @plans = @plans.where(templates: { family_id: template_ids }) if template_ids.present? - # filter on id(s) - plan_ids = extract_param_list(params, "plan") - @plans = @plans.where(id: plan_ids) if plan_ids.present? - # apply pagination after filtering - @args = { per_page: params[:per_page], page: params[:page] } - @plans = refine_query(@plans) - respond_with @plans - end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable - - private - - def extract_param_list(params, attribute) - list = params.fetch(attribute + "[]", []) - val = params.fetch(attribute, []) - list << val if val.present? - list - end - - # takes in the params hash and converts to a date-range - def dates_to_range(hash, start, stop) - today = Date.today - start_date = Date.parse(hash.fetch(start, today.prev_month.to_date.to_s)) - end_date = Date.parse(hash.fetch(stop, today.to_date.to_s)) + 1.day - start_date..end_date - end - end diff --git a/app/controllers/api/v0/statistics_controller.rb b/app/controllers/api/v0/statistics_controller.rb index b74f1a241d..2af50dfcb2 100644 --- a/app/controllers/api/v0/statistics_controller.rb +++ b/app/controllers/api/v0/statistics_controller.rb @@ -1,244 +1,243 @@ # frozen_string_literal: true -class Api::V0::StatisticsController < Api::V0::BaseController - - before_action :authenticate - - # GET /api/v0/statistics/users_joined?start_date=&end_date=&org_id= - # - # Returns the number of users joined for the user's org. - # If start_date is passed, only counts those with created_at is >= than start_date - # If end_date is passed, only counts those with created_at is <= than end_date are - # If org_id is passed and user has super_admin privileges that counter is performed - # against org_id param instead of user's org - - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - def users_joined - unless Api::V0::StatisticsPolicy.new(@user, :statistics).users_joined? - raise Pundit::NotAuthorizedError - end - - scoped = if @user.can_super_admin? && params[:org_id].present? - User.unscoped.where(org_id: params[:org_id]) - else - User.unscoped.where(org_id: @user.org_id) - end - - if params[:range_dates].present? - r = {} - params[:range_dates].each_pair do |k, v| - r[k] = scoped.where(created_at: dates_to_range(v)).count - end - - # Reverse hash r, so dates in ascending order - r = Hash[r.to_a.reverse] - - respond_to do |format| - format.json { render(json: r.to_json) } - format.csv do - send_data(CSV.generate do |csv| - csv << [_("Month"), _("No. Users joined")] - total = 0 - r.each_pair do |k, v| - csv << [k, v] - total += v +module Api + module V0 + # Provides statistical info for API V0 + class StatisticsController < Api::V0::BaseController + before_action :authenticate + + # GET /api/v0/statistics/users_joined?start_date=&end_date=&org_id= + # + # Returns the number of users joined for the user's org. + # If start_date is passed, only counts those with created_at is >= than start_date + # If end_date is passed, only counts those with created_at is <= than end_date are + # If org_id is passed and user has super_admin privileges that counter is performed + # against org_id param instead of user's org + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def users_joined + raise Pundit::NotAuthorizedError unless Api::V0::StatisticsPolicy.new(@user, :statistics).users_joined? + + scoped = if @user.can_super_admin? && params[:org_id].present? + User.unscoped.where(org_id: params[:org_id]) + else + User.unscoped.where(org_id: @user.org_id) + end + + if params[:range_dates].present? + r = {} + params[:range_dates].each_pair do |k, v| + r[k] = scoped.where(created_at: dates_to_range(v)).count + end + + # Reverse hash r, so dates in ascending order + r = r.to_a.reverse.to_h + + respond_to do |format| + format.json { render(json: r.to_json) } + format.csv do + send_data(CSV.generate do |csv| + csv << [_('Month'), _('No. Users joined')] + total = 0 + r.each_pair do |k, v| + csv << [k, v] + total += v + end + csv << [_('Total'), total] + end, filename: "#{_('users_joined')}.csv") end - csv << [_("Total"), total] - end, filename: "#{_('users_joined')}.csv") + end + else + if params['start_date'].present? || params['end_date'].present? + scoped = scoped.where(created_at: dates_to_range(params)) + end + @users_count = scoped.count + respond_with @users_count end end - else - if params["start_date"].present? || params["end_date"].present? - scoped = scoped.where(created_at: dates_to_range(params)) - end - @users_count = scoped.count - respond_with @users_count - end - end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable - - # GET - # Returns the number of completed plans within the user's org for the data - # start_date and end_date specified - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - def completed_plans - unless Api::V0::StatisticsPolicy.new(@user, :statistics).completed_plans? - raise Pundit::NotAuthorizedError - end - - scoped = if @user.can_super_admin? && params[:org_id].present? - Org.find(params[:org_id]).plans.where(complete: true) - else - @user.org.plans.where(complete: true) - end - - if params[:range_dates].present? - r = {} - params[:range_dates].each_pair do |k, v| - r[k] = scoped.where(created_at: dates_to_range(v)).count - end - - # Reverse hash r, so dates in ascending order - r = Hash[r.to_a.reverse] - - respond_to do |format| - format.json { render(json: r.to_json) } - format.csv do - send_data(CSV.generate do |csv| - csv << [_("Month"), _("No. Completed Plans")] - total = 0 - r.each_pair do |k, v| - csv << [k, v] - total += v + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + + # GET + # Returns the number of completed plans within the user's org for the data + # start_date and end_date specified + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def completed_plans + raise Pundit::NotAuthorizedError unless Api::V0::StatisticsPolicy.new(@user, :statistics).completed_plans? + + scoped = if @user.can_super_admin? && params[:org_id].present? + Org.find(params[:org_id]).plans.where(complete: true) + else + @user.org.plans.where(complete: true) + end + + if params[:range_dates].present? + r = {} + params[:range_dates].each_pair do |k, v| + r[k] = scoped.where(created_at: dates_to_range(v)).count + end + + # Reverse hash r, so dates in ascending order + r = r.to_a.reverse.to_h + + respond_to do |format| + format.json { render(json: r.to_json) } + format.csv do + send_data(CSV.generate do |csv| + csv << [_('Month'), _('No. Completed Plans')] + total = 0 + r.each_pair do |k, v| + csv << [k, v] + total += v + end + csv << [_('Total'), total] + end, filename: "#{_('completed_plans')}.csv") end - csv << [_("Total"), total] - end, filename: "#{_('completed_plans')}.csv") + end + else + if params['start_date'].present? || params['end_date'].present? + scoped = scoped.where(created_at: dates_to_range(params)) + end + render(json: { completed_plans: scoped.count }) end end - else - if params["start_date"].present? || params["end_date"].present? - scoped = scoped.where(created_at: dates_to_range(params)) - end - render(json: { completed_plans: scoped.count }) - end - end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable - - # /api/v0/statistics/created_plans - # Returns the number of created plans within the user's org for the data - # start_date and end_date specified - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - def created_plans - raise Pundit::NotAuthorizedError unless Api::V0::StatisticsPolicy.new(@user, :statistics).plans? - - scoped = if @user.can_super_admin? && params[:org_id].present? - Org.find(params[:org_id]).plans - else - @user.org.plans - end - - if params[:range_dates].present? - r = {} - params[:range_dates].each_pair do |k, v| - r[k] = scoped.where(created_at: dates_to_range(v)).count - end - - # Reverse hash r, so dates in ascending order - r = Hash[r.to_a.reverse] - - respond_to do |format| - format.json { render(json: r.to_json) } - format.csv do - send_data(CSV.generate do |csv| - csv << [_("Month"), _("No. Plans")] - total = 0 - r.each_pair do |k, v| - csv << [k, v] - total += v + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + + # /api/v0/statistics/created_plans + # Returns the number of created plans within the user's org for the data + # start_date and end_date specified + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def created_plans + raise Pundit::NotAuthorizedError unless Api::V0::StatisticsPolicy.new(@user, :statistics).plans? + + scoped = if @user.can_super_admin? && params[:org_id].present? + Org.find(params[:org_id]).plans + else + @user.org.plans + end + + if params[:range_dates].present? + r = {} + params[:range_dates].each_pair do |k, v| + r[k] = scoped.where(created_at: dates_to_range(v)).count + end + + # Reverse hash r, so dates in ascending order + r = r.to_a.reverse.to_h + + respond_to do |format| + format.json { render(json: r.to_json) } + format.csv do + send_data(CSV.generate do |csv| + csv << [_('Month'), _('No. Plans')] + total = 0 + r.each_pair do |k, v| + csv << [k, v] + total += v + end + csv << [_('Total'), total] + end, filename: "#{_('plans')}.csv") end - csv << [_("Total"), total] - end, filename: "#{_('plans')}.csv") + end + else + if params['start_date'].present? || params['end_date'].present? + scoped = scoped.where(created_at: dates_to_range(params)) + end + render(json: { completed_plans: scoped.count }) end end - else - if params["start_date"].present? || params["end_date"].present? - scoped = scoped.where(created_at: dates_to_range(params)) + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + + ## + # Displays the number of DMPs using templates owned/create by the caller's Org + # between the optional specified dates + # rubocop:disable Metrics/AbcSize + def using_template + org_templates = @user.org.templates.where(customization_of: nil) + raise Pundit::NotAuthorizedError unless Api::V0::StatisticsPolicy.new(@user, + org_templates.first).using_template? + + @templates = {} + org_templates.each do |template| + if @templates[template.title].blank? + @templates[template.title] = {} + @templates[template.title][:title] = template.title + @templates[template.title][:id] = template.family_id + @templates[template.title][:uses] = 0 + end + scoped = template.plans + if params['start_date'].present? || params['end_date'].present? + scoped = scoped.where(created_at: dates_to_range(params)) + end + @templates[template.title][:uses] += scoped.length + end + respond_with @templates end - render(json: { completed_plans: scoped.count }) - end - end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable - - ## - # Displays the number of DMPs using templates owned/create by the caller's Org - # between the optional specified dates - # rubocop:disable Metrics/AbcSize - def using_template - org_templates = @user.org.templates.where(customization_of: nil) - unless Api::V0::StatisticsPolicy.new(@user, org_templates.first).using_template? - raise Pundit::NotAuthorizedError - end - - @templates = {} - org_templates.each do |template| - if @templates[template.title].blank? - @templates[template.title] = {} - @templates[template.title][:title] = template.title - @templates[template.title][:id] = template.family_id - @templates[template.title][:uses] = 0 + # rubocop:enable Metrics/AbcSize + + ## + # GET + # Renders a list of templates with their titles, ids, and uses between the optional + # specified dates the uses are restricted to DMPs created by users of the same + # organisation as the user who ititiated the call. + # rubocop:disable Metrics/AbcSize + def plans_by_template + raise Pundit::NotAuthorizedError unless Api::V0::StatisticsPolicy.new(@user, :statistics).plans_by_template? + + @templates = {} + scoped = @user.org.plans + if params['start_date'].present? || params['end_date'].present? + scoped = scoped.where(created_at: dates_to_range(params)) + end + scoped.each do |plan| + # if hash exists + if @templates[plan.template.title].blank? + @templates[plan.template.title] = {} + @templates[plan.template.title][:title] = plan.template.title + @templates[plan.template.title][:id] = plan.template.family_id + @templates[plan.template.title][:uses] = 1 + else + @templates[plan.template.title][:uses] += 1 + end + end + respond_with @templates end - scoped = template.plans - if params["start_date"].present? || params["end_date"].present? - scoped = scoped.where(created_at: dates_to_range(params)) + # rubocop:enable Metrics/AbcSize + + # GET + # + # Renders a list of DMPs metadata, provided the DMPs were created between the + # optional specified dates DMPs must be owned by a user who's organisation is the + # same as the user who generates the call. + # rubocop:disable Metrics/AbcSize + def plans + raise Pundit::NotAuthorizedError unless Api::V0::StatisticsPolicy.new(@user, :statistics).plans? + + @org_plans = @user.org.plans + if params['remove_tests'].present? && params['remove_tests'].downcase == 'true' + @org_plans = @org_plans.where.not(visibility: Plan.visibilities[:is_test]) + end + if params['start_date'].present? || params['end_date'].present? + @org_plans = @org_plans.where(created_at: dates_to_range(params)) + end + respond_with @org_plans end - @templates[template.title][:uses] += scoped.length - end - respond_with @templates - end - # rubocop:enable Metrics/AbcSize - - ## - # GET - # Renders a list of templates with their titles, ids, and uses between the optional - # specified dates the uses are restricted to DMPs created by users of the same - # organisation as the user who ititiated the call. - # rubocop:disable Metrics/AbcSize - def plans_by_template - unless Api::V0::StatisticsPolicy.new(@user, :statistics).plans_by_template? - raise Pundit::NotAuthorizedError - end + # rubocop:enable Metrics/AbcSize - @templates = {} - scoped = @user.org.plans - if params["start_date"].present? || params["end_date"].present? - scoped = scoped.where(created_at: dates_to_range(params)) - end - scoped.each do |plan| - # if hash exists - if @templates[plan.template.title].blank? - @templates[plan.template.title] = {} - @templates[plan.template.title][:title] = plan.template.title - @templates[plan.template.title][:id] = plan.template.family_id - @templates[plan.template.title][:uses] = 1 - else - @templates[plan.template.title][:uses] += 1 + private + + # Convert start/end dates in hash to a range of Dates + def dates_to_range(hash) + today = Date.today + start_date = Date.parse(hash.fetch('start_date', today.prev_month.to_date.to_s)) + end_date = Date.parse(hash.fetch('end_date', today.to_date.to_s)) + 1.day + start_date..end_date end end - respond_with @templates - end - # rubocop:enable Metrics/AbcSize - - # GET - # - # Renders a list of DMPs metadata, provided the DMPs were created between the - # optional specified dates DMPs must be owned by a user who's organisation is the - # same as the user who generates the call. - # rubocop:disable Metrics/AbcSize - def plans - raise Pundit::NotAuthorizedError unless Api::V0::StatisticsPolicy.new(@user, :statistics).plans? - - @org_plans = @user.org.plans - if params["remove_tests"].present? && params["remove_tests"].downcase == "true" - @org_plans = @org_plans.where.not(visibility: Plan.visibilities[:is_test]) - end - if params["start_date"].present? || params["end_date"].present? - @org_plans = @org_plans.where(created_at: dates_to_range(params)) - end - respond_with @org_plans end - # rubocop:enable Metrics/AbcSize - - private - - # Convert start/end dates in hash to a range of Dates - def dates_to_range(hash) - today = Date.today - start_date = Date.parse(hash.fetch("start_date", today.prev_month.to_date.to_s)) - end_date = Date.parse(hash.fetch("end_date", today.to_date.to_s)) + 1.day - start_date..end_date - end - end diff --git a/app/controllers/api/v0/templates_controller.rb b/app/controllers/api/v0/templates_controller.rb index b4f8e1e243..f1156593f5 100644 --- a/app/controllers/api/v0/templates_controller.rb +++ b/app/controllers/api/v0/templates_controller.rb @@ -1,58 +1,59 @@ # frozen_string_literal: true -class Api::V0::TemplatesController < Api::V0::BaseController - - before_action :authenticate - - # GET - # - # Renders a list of templates ordered by organisation - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - def index - # check if the user has permissions to use the templates API - unless Api::V0::TemplatePolicy.new(@user, :guidance_group).index? - raise Pundit::NotAuthorizedError - end - - @org_templates = {} - - published_templates = Template.includes(:org) - .unarchived - .where(customization_of: nil, published: true) - .order(:org_id, :version) - - customized_templates = Template.includes(:org) - .unarchived - .where(org_id: @user.org_id, published: true) - .where.not(customization_of: nil) - - published_templates.order(:org_id, :version).each do |temp| - if @org_templates[temp.org].present? - if @org_templates[temp.org][:own][temp.family_id].nil? - @org_templates[temp.org][:own][temp.family_id] = temp +module Api + module V0 + # Handles queries for templates for API V0 + class TemplatesController < Api::V0::BaseController + before_action :authenticate + + # GET + # + # Renders a list of templates ordered by organisation + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def index + # check if the user has permissions to use the templates API + raise Pundit::NotAuthorizedError unless Api::V0::TemplatePolicy.new(@user, :guidance_group).index? + + @org_templates = {} + + published_templates = Template.includes(:org) + .unarchived + .where(customization_of: nil, published: true) + .order(:org_id, :version) + + customized_templates = Template.includes(:org) + .unarchived + .where(org_id: @user.org_id, published: true) + .where.not(customization_of: nil) + + published_templates.order(:org_id, :version).each do |temp| + if @org_templates[temp.org].present? + @org_templates[temp.org][:own][temp.family_id] = temp if @org_templates[temp.org][:own][temp.family_id].nil? + else + @org_templates[temp.org] = {} + @org_templates[temp.org][:own] = {} + @org_templates[temp.org][:cust] = {} + @org_templates[temp.org][:own][temp.family_id] = temp + end end - else - @org_templates[temp.org] = {} - @org_templates[temp.org][:own] = {} - @org_templates[temp.org][:cust] = {} - @org_templates[temp.org][:own][temp.family_id] = temp - end - end - customized_templates.each do |temp| - if @org_templates[temp.org].present? - if @org_templates[temp.org][:cust][temp.family_id].nil? - @org_templates[temp.org][:cust][temp.family_id] = temp + customized_templates.each do |temp| + if @org_templates[temp.org].present? + if @org_templates[temp.org][:cust][temp.family_id].nil? + @org_templates[temp.org][:cust][temp.family_id] = + temp + end + else + @org_templates[temp.org] = {} + @org_templates[temp.org][:own] = {} + @org_templates[temp.org][:cust] = {} + @org_templates[temp.org][:cust][temp.family_id] = temp + end end - else - @org_templates[temp.org] = {} - @org_templates[temp.org][:own] = {} - @org_templates[temp.org][:cust] = {} - @org_templates[temp.org][:cust][temp.family_id] = temp + respond_with @org_templates end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity end - respond_with @org_templates end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable - end diff --git a/app/controllers/api/v1/authentication_controller.rb b/app/controllers/api/v1/authentication_controller.rb index 752bfbb117..c74dd39c5d 100644 --- a/app/controllers/api/v1/authentication_controller.rb +++ b/app/controllers/api/v1/authentication_controller.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module Api - module V1 - # Accepts 2 types of authentication: # # Client Credentials: @@ -24,12 +22,12 @@ module V1 # code: "[users.api_token]" # } class AuthenticationController < BaseApiController - respond_to :json skip_before_action :authorize_request, only: %i[authenticate] # POST /api/v1/authenticate + # rubocop:disable Metrics/AbcSize def authenticate body = request.body.read json = JSON.parse(body) @@ -38,20 +36,17 @@ def authenticate if @token.present? @expiration = auth_svc.expiration - @token_type = "Bearer" - render "/api/v1/token", status: :ok + @token_type = 'Bearer' + render '/api/v1/token', status: :ok else render_error errors: auth_svc.errors, status: :unauthorized end rescue JSON::ParserError => e Rails.logger.error "API V1 - authenticate: #{e.message}" Rails.logger.error request.body.read - render_error errors: _("Missing or invalid JSON"), status: :bad_request + render_error errors: _('Missing or invalid JSON'), status: :bad_request end - # rubocop:enable - + # rubocop:enable Metrics/AbcSize end - end - end diff --git a/app/controllers/api/v1/base_api_controller.rb b/app/controllers/api/v1/base_api_controller.rb index 77da394d82..c1ddc36547 100644 --- a/app/controllers/api/v1/base_api_controller.rb +++ b/app/controllers/api/v1/base_api_controller.rb @@ -1,12 +1,9 @@ # frozen_string_literal: true module Api - module V1 - # Base API Controller class BaseApiController < ApplicationController - # Skipping the standard Rails authenticity tokens passed in UI skip_before_action :verify_authenticity_token @@ -26,14 +23,14 @@ class BaseApiController < ApplicationController # GET /api/v1/heartbeat def heartbeat - render "/api/v1/heartbeat", status: :ok + render '/api/v1/heartbeat', status: :ok end protected def render_error(errors:, status:) @payload = { errors: [errors] } - render "/api/v1/error", status: status + render '/api/v1/error', status: status end private @@ -63,12 +60,14 @@ def base_response_content # Retrieve the requested pagination params or use defaults # only allow 100 per page as the max def pagination_params - @page = params.fetch("page", 1).to_i - @per_page = params.fetch("per_page", 20).to_i - @per_page = 100 if @per_page > 100 + 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 # Parse the body of the incoming request + # rubocop:disable Metrics/AbcSize def parse_request return false unless request.present? && request.body.present? @@ -78,10 +77,11 @@ def parse_request rescue JSON::ParserError => e Rails.logger.error "JSON Parser: #{e.message}" Rails.logger.error request.body - render_error(errors: _("Invalid JSON format"), status: :bad_request) + render_error(errors: _('Invalid JSON format'), status: :bad_request) false end end + # rubocop:enable Metrics/AbcSize # ========================== @@ -189,9 +189,6 @@ def host_permitted_params storage_type availability geo_location certified_with pid_system] + [host_ids: identifier_permitted_params] end - end - end - end diff --git a/app/controllers/api/v1/plans_controller.rb b/app/controllers/api/v1/plans_controller.rb index 649f987d97..b1480437bd 100644 --- a/app/controllers/api/v1/plans_controller.rb +++ b/app/controllers/api/v1/plans_controller.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true module Api - module V1 - + # Handles CRUD operations for plans in API V1 class PlansController < BaseApiController - respond_to :json # GET /api/v1/plans/:id @@ -15,14 +13,15 @@ def show if plans.present? && plans.any? @items = paginate_response(results: plans) - render "/api/v1/plans/index", status: :ok + render '/api/v1/plans/index', status: :ok else - render_error(errors: [_("Plan not found")], status: :not_found) + render_error(errors: [_('Plan not found')], status: :not_found) end end # POST /api/v1/plans # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def create dmp = @json.with_indifferent_access.fetch(:items, []).first.fetch(:dmp, {}) @@ -34,8 +33,8 @@ def create # Convert the JSON into a Plan and it's associations plan = Api::V1::Deserialization::Plan.deserialize(json: dmp) if plan.present? - save_err = _("Unable to create your DMP") - exists_err = _("Plan already exists. Send an update instead.") + save_err = _('Unable to create your DMP') + exists_err = _('Plan already exists. Send an update instead.') no_org_err = _("Could not determine ownership of the DMP. Please add an :affiliation to the :contact") @@ -55,11 +54,8 @@ def create # If we cannot save for some reason then return an error plan = Api::V1::PersistenceService.safe_save(plan: plan) - # rubocop:disable Layout/LineLength render_error(errors: save_err, status: :internal_server_error) and return if plan.new_record? - # rubocop:enable Layout/LineLength - # If the plan was generated by an ApiClient then associate them plan.update(api_client_id: client.id) if client.is_a?(ApiClient) @@ -69,14 +65,15 @@ def create # Kaminari Pagination requires an ActiveRecord result set :/ @items = paginate_response(results: Plan.where(id: plan.id)) - render "/api/v1/plans/index", status: :created + render '/api/v1/plans/index', status: :created else - render_error(errors: [_("Invalid JSON!")], status: :bad_request) + render_error(errors: [_('Invalid JSON!')], status: :bad_request) end rescue JSON::ParserError - render_error(errors: [_("Invalid JSON")], status: :bad_request) + render_error(errors: [_('Invalid JSON')], status: :bad_request) end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # GET /api/v1/plans def index @@ -88,9 +85,9 @@ def index if plans.present? && plans.any? @items = paginate_response(results: plans) @minimal = true - render "api/v1/plans/index", status: :ok + render 'api/v1/plans/index', status: :ok else - render_error(errors: [_("No Plans found")], status: :not_found) + render_error(errors: [_('No Plans found')], status: :not_found) end end @@ -134,11 +131,12 @@ def lookup_user(contributor:) user end + # rubocop:disable Metrics/AbcSize def invite_contributor(contributor:) return nil unless contributor.present? # If the user was not found, invite them and attach any know identifiers - names = contributor.name&.split || [""] + names = contributor.name&.split || [''] firstname = names.length > 1 ? names.first : nil surname = names.length > 1 ? names.last : names.first user = User.invite!({ email: contributor.email, @@ -153,9 +151,7 @@ def invite_contributor(contributor:) end user end - + # rubocop:enable Metrics/AbcSize end - end - end diff --git a/app/controllers/api/v1/templates_controller.rb b/app/controllers/api/v1/templates_controller.rb index 57383c825c..729b3edbbd 100644 --- a/app/controllers/api/v1/templates_controller.rb +++ b/app/controllers/api/v1/templates_controller.rb @@ -1,14 +1,13 @@ # frozen_string_literal: true module Api - module V1 - + # Provides a list of templates for API V1 class TemplatesController < BaseApiController - respond_to :json # GET /api/v1/templates + # rubocop:disable Metrics/AbcSize def index # If this is a User and not an ApiClient include the Org's # templates and customizations as well as the public ones @@ -35,12 +34,10 @@ def index templates = templates.order(:title) @items = paginate_response(results: templates) - render "/api/v1/templates/index", status: :ok + render '/api/v1/templates/index', status: :ok end # rubocop:enable - end - + # rubocop:enable Metrics/AbcSize end - end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a4bbf559c5..da8cde06da 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true +# Base controller logic class ApplicationController < ActionController::Base - - protect_from_forgery with: :exception before_action :configure_permitted_parameters, if: :devise_controller? @@ -15,7 +14,7 @@ class ApplicationController < ActionController::Base after_action :store_location include GlobalHelpers - include Pundit + include Pundit::Authorization helper_method GlobalHelpers.instance_methods rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized @@ -23,8 +22,6 @@ class ApplicationController < ActionController::Base # When we are in production reroute Record Not Found errors to the branded 404 page rescue_from ActiveRecord::RecordNotFound, with: :render_not_found - rescue_from StandardError, with: :handle_server_error - rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private @@ -36,11 +33,11 @@ def current_org def user_not_authorized if user_signed_in? # redirect_to plans_url, alert: _("You are not authorized to perform this action.") - msg = _("You are not authorized to perform this action.") + msg = _('You are not authorized to perform this action.') render_respond_to_format_with_error_message(msg, plans_url, 403, nil) else # redirect_to root_url, alert: _("You need to sign in or sign up before continuing.") - msg = _("You need to sign in or sign up before continuing.") + msg = _('You need to sign in or sign up before continuing.') render_respond_to_format_with_error_message(msg, root_url, 401, nil) end end @@ -57,15 +54,16 @@ def current_locale def store_location # store last url - this is needed for post-login redirect to whatever the user last # visited. - unless ["/users/sign_in", - "/users/sign_up", - "/users/password", - "/users/invitation/accept"].any? { |ur| request.fullpath.include?(ur) } \ - or request.xhr? # don't store ajax calls + unless ['/users/sign_in', + '/users/sign_up', + '/users/password', + '/users/invitation/accept'].any? { |ur| request.fullpath.include?(ur) } \ + || request.xhr? # don't store ajax calls session[:previous_url] = request.fullpath 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) || @@ -76,6 +74,7 @@ def after_sign_in_path_for(_resource) request.referer end end + # rubocop:enable Metrics/AbcSize def after_sign_up_path_for(_resource) referer_path = URI(request.referer).path unless request.referer.nil? @@ -92,8 +91,8 @@ def after_sign_in_error_path_for(_resource) (from_external_domain? ? root_path : request.referer || root_path) end - def after_sign_up_error_path_for(resource) - root_path(anchor: 'create-account-form') + def after_sign_up_error_path_for(_resource) + (from_external_domain? ? root_path : request.referer || root_path) end def authenticate_admin! @@ -105,45 +104,42 @@ def authenticate_admin! end end - def failure_message(obj, action = "save") - _("Unable to %{action} the %{object}.%{errors}") % { - object: obj_name_for_display(obj), - action: action || "save", - errors: errors_for_display(obj) - } + def failure_message(obj, action = 'save') + format(_('Unable to %{action} the %{object}. %{errors}'), + object: obj_name_for_display(obj), + action: action || 'save', errors: errors_for_display(obj)) end - def success_message(obj, action = "saved") - _("Successfully %{action} the %{object}.") % { - object: obj_name_for_display(obj), - action: action || "save" - } + def success_message(obj, action = 'saved') + format(_('Successfully %{action} the %{object}.'), object: obj_name_for_display(obj), action: action || 'save') end def errors_for_display(obj) - return "" unless obj.present? && obj.errors.any? + return '' unless obj.present? && obj.errors.any? msgs = obj.errors.full_messages.uniq.collect { |msg| "
  • #{msg}
  • " } - "
      #{msgs.join('')}
    " + "
      #{msgs.join}
    " end + # rubocop:disable Metrics/AbcSize def obj_name_for_display(obj) display_name = { - ApiClient: _("API client"), - ExportedPlan: _("plan"), - GuidanceGroup: _("guidance group"), - Note: _("comment"), - Org: _("organisation"), - Perm: _("permission"), - Pref: _("preferences"), - User: obj == current_user ? _("profile") : _("user"), - QuestionOption: _("question option") + ApiClient: _('API client'), + ExportedPlan: _('plan'), + GuidanceGroup: _('guidance group'), + Note: _('comment'), + Org: _('organisation'), + Perm: _('permission'), + Pref: _('preferences'), + User: obj == current_user ? _('profile') : _('user'), + QuestionOption: _('question option') } if obj.respond_to?(:customization_of) && obj.send(:customization_of).present? - display_name[:Template] = "customization" + display_name[:Template] = 'customization' end - display_name[obj.class.name.to_sym] || obj.class.name.downcase || "record" + display_name[obj.class.name.to_sym] || obj.class.name.downcase || 'record' end + # rubocop:enable Metrics/AbcSize # Override rails default render action to look for a branded version of a # template instead of using the default one. If no override exists, the @@ -153,7 +149,7 @@ def obj_name_for_display(obj) # replacing. For example: # app/views/branded/layouts/_header.html.erb -> app/views/layouts/_header.html.erb def prepend_view_paths - prepend_view_path Rails.root.join("app", "views", "branded") + prepend_view_path Rails.root.join('app', 'views', 'branded') end ## @@ -182,15 +178,10 @@ def configure_permitted_parameters end def render_not_found(exception) - msg = _("Record Not Found") + ": #{exception.message}" + msg = _('Record Not Found') + ": #{exception.message}" render_respond_to_format_with_error_message(msg, root_url, 404, exception) end - def handle_server_error(exception) - msg = exception.message.to_s if exception.present? - render_respond_to_format_with_error_message(msg, root_url, 500, exception) - end - def render_respond_to_format_with_error_message(msg, url_or_path, http_status, exception) Rails.logger.error msg Rails.logger.error exception&.backtrace if exception.present? @@ -201,10 +192,8 @@ def render_respond_to_format_with_error_message(msg, url_or_path, http_status, e # Render the JSON error message (using API V1) format.json do @payload = { errors: [msg] } - render "/api/v1/error", status: http_status + render '/api/v1/error', status: http_status end end - end - end diff --git a/app/controllers/concerns/allowed_question_formats.rb b/app/controllers/concerns/allowed_question_formats.rb index 0dc68f3af7..3e8d4bd995 100644 --- a/app/controllers/concerns/allowed_question_formats.rb +++ b/app/controllers/concerns/allowed_question_formats.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true +# Controller that gets Questions types that allow multiple selections +# TODO: this could likely just live on the model! module AllowedQuestionFormats - private # The QuestionFormat "Multi select box" is no longer being used for new templates def allowed_question_formats - QuestionFormat.where.not(title: "Multi select box").order(:title) + QuestionFormat.where.not(title: 'Multi select box').order(:title) end - end diff --git a/app/controllers/concerns/conditional_user_mailer.rb b/app/controllers/concerns/conditional_user_mailer.rb index c9f5ce42e1..c83338347d 100644 --- a/app/controllers/concerns/conditional_user_mailer.rb +++ b/app/controllers/concerns/conditional_user_mailer.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true +# Determines whether or not the user has enabled/disabled the email notification +# before sending it out module ConditionalUserMailer - # Executes a given block passed if the recipient user has the preference # email key enabled # @@ -10,18 +11,17 @@ module ConditionalUserMailer # prefences.email (see dmproadmap.rb initializer) # # Returns Boolean - def deliver_if(recipients: [], key:, &block) + def deliver_if(key:, recipients: [], &block) return false unless block_given? Array(recipients).each do |recipient| - email_hash = recipient.get_preferences("email").with_indifferent_access + email_hash = recipient.get_preferences('email').with_indifferent_access # Violation of rubocop's DoubleNegation check # preference_value = !!email_hash.dig(*key.to_s.split(".")) - preference_value = email_hash.dig(*key.to_s.split(".")) + preference_value = email_hash.dig(*key.to_s.split('.')) block.call(recipient) if preference_value end true end - end diff --git a/app/controllers/concerns/org_selectable.rb b/app/controllers/concerns/org_selectable.rb index 5aac93cc48..84f3d270a4 100644 --- a/app/controllers/concerns/org_selectable.rb +++ b/app/controllers/concerns/org_selectable.rb @@ -54,7 +54,6 @@ # # See the comments on OrgsController#search for more info on how the typeaheads work module OrgSelectable - extend ActiveSupport::Concern # rubocop:disable Metrics/BlockLength @@ -65,7 +64,9 @@ module OrgSelectable # Converts the incoming params_into an Org by either locating it # via its id, identifier and/or name, or initializing a new one - def org_from_params(params_in:, allow_create: true) + # the default allow_create is based off restrict_orgs + def org_from_params(params_in:, + allow_create: !Rails.configuration.x.application.restrict_orgs) # params_in = params_in.with_indifferent_access return nil unless params_in[:org_id].present? && params_in[:org_id].is_a?(String) @@ -125,11 +126,10 @@ def create_org(org:, params_in:) end def prep_org_partial - name = Rails.configuration.x.application.restrict_orgs ? "local_only" : "combined" + name = Rails.configuration.x.application.restrict_orgs ? 'local_only' : 'combined' @org_partial = "shared/org_selectors/#{name}" @all_orgs = Org.includes(identifiers: [:identifier_scheme]).all end end # rubocop:enable Metrics/BlockLength - end diff --git a/app/controllers/concerns/paginable.rb b/app/controllers/concerns/paginable.rb index aea376ed12..c3cfa1c923 100644 --- a/app/controllers/concerns/paginable.rb +++ b/app/controllers/concerns/paginable.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true +# Provides support for pagination/searching/sorting of table data # rubocop:disable Metrics/ModuleLength module Paginable - extend ActiveSupport::Concern - require "sort_direction" + require 'sort_direction' ## # Regex to validate sort_field param is safe - SORT_COLUMN_FORMAT = /[\w_]+\.[\w_]/.freeze + SORT_COLUMN_FORMAT = /[\w_]+\.[\w_]+$/.freeze PAGINATION_QUERY_PARAMS = %i[page sort_field sort_direction search controller action].freeze @@ -37,15 +37,16 @@ module Paginable # one approach to just include everything in the double splat `**options` param # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def paginable_renderise(partial: nil, template: nil, controller: nil, action: nil, path_params: {}, query_params: {}, scope: nil, locals: {}, **options) unless scope.is_a?(ActiveRecord::Relation) - raise ArgumentError, _("scope should be an ActiveRecord::Relation object") + raise ArgumentError, _('scope should be an ActiveRecord::Relation object') end - raise ArgumentError, _("path_params should be a Hash object") unless path_params.is_a?(Hash) - raise ArgumentError, _("query_params should be a Hash object") unless query_params.is_a?(Hash) - raise ArgumentError, _("locals should be a Hash object") unless locals.is_a?(Hash) + raise ArgumentError, _('path_params should be a Hash object') unless path_params.is_a?(Hash) + raise ArgumentError, _('query_params should be a Hash object') unless query_params.is_a?(Hash) + raise ArgumentError, _('locals should be a Hash object') unless locals.is_a?(Hash) # Default options @paginable_options = {}.merge(options) @@ -61,12 +62,12 @@ def paginable_renderise(partial: nil, template: nil, controller: nil, action: ni # Additional path_params passed to this function got special treatment # (e.g. it is taking into account when building base_url) @paginable_path_params = path_params.symbolize_keys - if @args[:page] == "ALL" && + if @args[:page] == 'ALL' && @args[:search].blank? && @paginable_options[:view_all] == false render( status: :forbidden, - html: _("Restricted access to View All the records") + html: _('Restricted access to View All the records') ) else @refined_scope = refine_query(scope) @@ -78,17 +79,17 @@ def paginable_renderise(partial: nil, template: nil, controller: nil, action: ni ) # If this was an ajax call then render as JSON if options[:format] == :json - render json: { html: render_to_string(layout: "/layouts/paginable", + render json: { html: render_to_string(layout: '/layouts/paginable', partial: partial, locals: locals) } elsif partial.present? - render(layout: "/layouts/paginable", partial: partial, locals: locals) + render(layout: '/layouts/paginable', partial: partial, locals: locals) else render(template: template, locals: locals) end end end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # Returns the base url of the paginable route for a given page passed def paginable_base_url(page = 1) @@ -107,7 +108,7 @@ def paginable_sort_link(sort_field) link_to( sort_link_name(sort_field), sort_link_url(sort_field), - class: "paginable-action", + class: 'paginable-action', data: { remote: @paginable_options[:remote] }, aria: { label: sort_field } ) @@ -125,21 +126,21 @@ def paginable? # Refine a scope passed to this concern if any of the params (search, # sort_field or page) are present - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def refine_query(scope) @args = @args.with_indifferent_access - scope = scope.search(@args[:search]) if @args[:search].present? + scope = scope.search(@args[:search]).distinct if @args[:search].present? # Can raise NoMethodError if the scope does not define a search method if @args[:sort_field].present? frmt = @args[:sort_field][SORT_COLUMN_FORMAT] - raise ArgumentError, "sort_field param looks unsafe" unless frmt + raise ArgumentError, 'sort_field param looks unsafe' unless frmt # Can raise ActiveRecord::StatementInvalid (e.g. column does not # exist, ambiguity on column, etc) # how we contruct scope depends on whether sort field is in the # main table or in a related table scope_table = scope.klass.name.underscore - parts = @args[:sort_field].partition(".") + parts = @args[:sort_field].partition('.') table_part = parts.first column_part = parts.last if scope_table == table_part.singularize @@ -147,17 +148,18 @@ def refine_query(scope) scope = scope.order(order_field.to_sym => sort_direction.to_s) else order_field = ActiveRecord::Base.sanitize_sql(@args[:sort_field]) + sd = ActiveRecord::Base.sanitize_sql(sort_direction) scope = scope.includes(table_part.singularize.to_sym) - .order(order_field + " " + sort_direction.to_s) + .order("#{order_field} #{sd}") end end - if @args[:page] != "ALL" + if @args[:page] != 'ALL' # Can raise error if page is not a number scope = scope.page(@args[:page]) - end + .per(@args.fetch(:per_page, Rails.configuration.x.results_per_page)) end scope end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength def sort_direction @sort_direction ||= SortDirection.new(@args[:sort_direction]) @@ -167,9 +169,9 @@ def sort_direction # html prevented of being escaped def sort_link_name(sort_field) @args = @args.with_indifferent_access - class_name = "fas fa-sort" - dir = "up" - dir = "down" if sort_direction.to_s == "DESC" + class_name = 'fas fa-sort' + dir = 'up' + dir = 'down' if sort_direction.to_s == 'DESC' class_name = "fas fa-sort-#{dir}" if @args[:sort_field] == sort_field <<~HTML.html_safe - #{_('Sort by %{sort_field}') % { sort_field: sort_field.split('.').first }} + #{format(_('Sort by %{sort_field}'), sort_field: sort_field.split('.').first)} HTML end # Returns the sort url for a given sort_field. + # rubocop:disable Metrics/AbcSize def sort_link_url(sort_field) @args = @args.with_indifferent_access query_params = {} - query_params[:page] = @args[:page] == "ALL" ? "ALL" : 1 + query_params[:page] = @args[:page] == 'ALL' ? 'ALL' : 1 query_params[:sort_field] = sort_field query_params[:sort_direction] = if @args[:sort_field] == sort_field sort_direction.opposite @@ -200,6 +203,7 @@ def sort_link_url(sort_field) sort_url.to_s "#{sort_url}&#{stringify_nonpagination_query_params}" end + # rubocop:enable Metrics/AbcSize # Retrieve any query params that are not a part of the paginable concern def stringify_nonpagination_query_params @@ -211,10 +215,10 @@ def stringify_query_params(page: 1, search: @args[:search], sort_direction: nil) query_string = { page: page } - query_string["search"] = search if search.present? + query_string['search'] = search if search.present? if sort_field.present? - query_string["sort_field"] = sort_field - query_string["sort_direction"] = SortDirection.new(sort_direction) + query_string['sort_field'] = sort_field + query_string['sort_direction'] = SortDirection.new(sort_direction) end query_string.to_param end @@ -222,6 +226,5 @@ def stringify_query_params(page: 1, search: @args[:search], def paginable_params params.permit(PAGINATION_QUERY_PARAMS) end - end # rubocop:enable Metrics/ModuleLength diff --git a/app/controllers/concerns/template_methods.rb b/app/controllers/concerns/template_methods.rb index bebd8249a2..f1ebf5d862 100644 --- a/app/controllers/concerns/template_methods.rb +++ b/app/controllers/concerns/template_methods.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true # This module holds helper controller methods for controllers that deal with Templates -# module TemplateMethods - private def template_type(template) - template.customization_of.present? ? _("customisation") : _("template") + template.customization_of.present? ? _('customisation') : _('template') end - end diff --git a/app/controllers/concerns/versionable.rb b/app/controllers/concerns/versionable.rb index 24233e4ef9..e242f16e78 100644 --- a/app/controllers/concerns/versionable.rb +++ b/app/controllers/concerns/versionable.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Helpers that allow us to version Template-Phase-Section-Question module Versionable - private # Takes in a Template, phase, Section, Question, or Annotaion @@ -18,7 +18,7 @@ def get_modifiable(obj) template = obj else raise ArgumentError, - _("obj should be a Template, Phase, Section, Question, or Annotation") + _('obj should be a Template, Phase, Section, Question, or Annotation') end # raises RuntimeError if template is not latest @@ -39,11 +39,12 @@ def get_modifiable(obj) # generated and returns a modifiable version of that object # NOTE: the obj passed is still not saved however it should belongs to a # parent already - # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity def get_new(obj) unless obj.respond_to?(:template) raise ArgumentError, - _("obj should be a Phase, Section, Question, or Annotation") + _('obj should be a Phase, Section, Question, or Annotation') end template = obj.template @@ -62,7 +63,7 @@ def get_new(obj) belongs = :question else raise ArgumentError, - _("obj should be a Phase, Section, Question, or Annotation") + _('obj should be a Phase, Section, Question, or Annotation') end if belongs == :template @@ -76,21 +77,21 @@ def get_new(obj) end obj end - # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity # Locates an object (e.g. phase, section, question, annotation) in a # search_space # (e.g. phases/sections/questions/annotations) by comparing either the number # method or the org_id and text for annotations # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def find_in_space(obj, search_space) - unless search_space.respond_to?(:each) - raise ArgumentError, _("The search_space does not respond to each") - end + raise ArgumentError, _('The search_space does not respond to each') unless search_space.respond_to?(:each) if search_space.empty? raise ArgumentError, - _("The search space does not have elements associated") + _('The search space does not have elements associated') end if obj.is_a?(search_space.first.class) @@ -131,6 +132,5 @@ def find_in_space(obj, search_space) nil end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable - + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity end diff --git a/app/controllers/contacts_controller.rb b/app/controllers/contacts_controller.rb index 9857fb6c1b..ca95d2fa3d 100644 --- a/app/controllers/contacts_controller.rb +++ b/app/controllers/contacts_controller.rb @@ -1,39 +1,43 @@ # frozen_string_literal: true -class ContactUs::ContactsController < ApplicationController +module ContactUs + # Controller for the Contact Us gem + class ContactsController < ApplicationController + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def create + @contact = ContactUs::Contact.new(params[:contact_us_contact]) - def create - @contact = ContactUs::Contact.new(params[:contact_us_contact]) - - if !user_signed_in? && Rails.configuration.x.recaptcha.enabled - unless verify_recaptcha(model: @contact) && @contact.save - flash[:alert] = _("Captcha verification failed, please retry.") + if !user_signed_in? && Rails.configuration.x.recaptcha.enabled && + !(verify_recaptcha(model: @contact) && @contact.save) + flash[:alert] = _('Captcha verification failed, please retry.') render_new_page and return end + if @contact.save + redirect_to(ContactUs.success_redirect || '/', + notice: _('Contact email was successfully sent.')) + else + flash[:alert] = _('Unable to submit your request') + render_new_page + end end - if @contact.save - redirect_to(ContactUs.success_redirect || "/", - notice: _("Contact email was successfully sent.")) - else - flash[:alert] = _("Unable to submit your request") + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + + def new + @contact = ContactUs::Contact.new render_new_page end - end - def new - @contact = ContactUs::Contact.new - render_new_page - end - - protected + protected - def render_new_page - case ContactUs.form_gem - when "formtastic" then render "new_formtastic" - when "simple_form" then render "new_simple_form" - else - render "new" + def render_new_page + case ContactUs.form_gem + when 'formtastic' then render 'new_formtastic' + when 'simple_form' then render 'new_simple_form' + else + render 'new' + end end end - end diff --git a/app/controllers/contributors_controller.rb b/app/controllers/contributors_controller.rb index d5246f9a5b..3c210b63d4 100644 --- a/app/controllers/contributors_controller.rb +++ b/app/controllers/contributors_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Controller for the Contributors page class ContributorsController < ApplicationController - include OrgSelectable helper PaginableHelper @@ -27,17 +27,17 @@ def edit authorize @plan end - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength # POST /plans/:plan_id/contributors def create - authorize @plan + authorize @plan, :edit? args = translate_roles(hash: contributor_params) args = process_org(hash: args) if args.blank? @contributor = Contributor.new(args) - @contributor.errors.add(:affiliation, "invalid") - flash[:alert] = failure_message(@contributor, _("add")) + @contributor.errors.add(:affiliation, 'invalid') + flash[:alert] = failure_message(@contributor, _('add')) render :new else args = process_orcid_for_create(hash: args) @@ -51,14 +51,14 @@ def create save_orcid redirect_to plan_contributors_path(@plan), - notice: success_message(@contributor, _("added")) + notice: success_message(@contributor, _('added')) else - flash[:alert] = failure_message(@contributor, _("add")) + flash[:alert] = failure_message(@contributor, _('add')) render :new end end end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength # PUT /plans/:plan_id/contributors/:id def update @@ -69,9 +69,9 @@ def update if @contributor.update(args) redirect_to edit_plan_contributor_path(@plan, @contributor), - notice: success_message(@contributor, _("saved")) + notice: success_message(@contributor, _('saved')) else - flash.now[:alert] = failure_message(@contributor, _("save")) + flash.now[:alert] = failure_message(@contributor, _('save')) render :edit end end @@ -81,10 +81,10 @@ def update def destroy authorize @plan if @contributor.destroy - msg = success_message(@contributor, _("removed")) + msg = success_message(@contributor, _('removed')) redirect_to plan_contributors_path(@plan), notice: msg else - flash.now[:alert] = failure_message(@contributor, _("remove")) + flash.now[:alert] = failure_message(@contributor, _('remove')) render :edit end end @@ -105,7 +105,7 @@ def contributor_params # Translate the check boxes values of "1" and "0" to true/false def translate_roles(hash:) roles = Contributor.new.all_roles - roles.each { |role| hash[role.to_sym] = hash[role.to_sym] == "1" } + roles.each { |role| hash[role.to_sym] = hash[role.to_sym] == '1' } hash end @@ -117,9 +117,10 @@ def process_org(hash:) allow = !Rails.configuration.x.application.restrict_orgs org = org_from_params(params_in: hash, allow_create: allow) - return nil if org.blank? && !allow hash = remove_org_selection_params(params_in: hash) + + return hash if org.blank? && !allow return hash unless org.present? hash[:org_id] = org.id @@ -130,7 +131,7 @@ def process_org(hash:) def process_orcid_for_create(hash:) return hash unless hash[:identifiers_attributes].present? - id_hash = hash[:identifiers_attributes][:"0"] + id_hash = hash[:identifiers_attributes][:'0'] return hash unless id_hash[:value].blank? hash.delete(:identifiers_attributes) @@ -141,10 +142,10 @@ def process_orcid_for_create(hash:) def process_orcid_for_update(hash:) return hash unless hash[:identifiers_attributes].present? - id_hash = hash[:identifiers_attributes][:"0"] + id_hash = hash[:identifiers_attributes][:'0'] return hash unless id_hash[:value].blank? - existing = @contributor.identifier_for_scheme(scheme: "orcid") + existing = @contributor.identifier_for_scheme(scheme: 'orcid') existing.destroy if existing.present? hash.delete(:identifiers_attributes) hash @@ -157,7 +158,7 @@ def fetch_plan @plan = Plan.includes(:contributors).find_by(id: params[:plan_id]) return true if @plan.present? - redirect_to root_path, alert: _("plan not found") + redirect_to root_path, alert: _('plan not found') end def fetch_contributor @@ -165,7 +166,7 @@ def fetch_contributor return true if @contributor.present? && @plan.contributors.include?(@contributor) - redirect_to plan_contributors_path, alert: _("contributor not found") + redirect_to plan_contributors_path, alert: _('contributor not found') end # The following 2 methods address an issue with using Rails normal @@ -195,5 +196,4 @@ def save_orcid @cached_orcid.save @contributor.reload end - end diff --git a/app/controllers/feedback_requests_controller.rb b/app/controllers/feedback_requests_controller.rb index 21ad271218..a8d91c4976 100644 --- a/app/controllers/feedback_requests_controller.rb +++ b/app/controllers/feedback_requests_controller.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true +# Controller that handles requests for Admin Feedback class FeedbackRequestsController < ApplicationController - include FeedbacksHelper after_action :verify_authorized - ALERT = _("Unable to submit your request for feedback at this time.") - ERROR = _("An error occurred when requesting feedback for this plan.") + ALERT = _('Unable to submit your request for feedback at this time.') + ERROR = _('An error occurred when requesting feedback for this plan.') def create @plan = Plan.find(params[:plan_id]) @@ -34,5 +34,4 @@ def request_feedback_flash_notice text = current_user.org.feedback_msg || feedback_confirmation_default_message feedback_constant_to_text(text, current_user, @plan, current_user.org) end - end diff --git a/app/controllers/guidance_groups_controller.rb b/app/controllers/guidance_groups_controller.rb index a20b2dd233..611d5ef4d4 100644 --- a/app/controllers/guidance_groups_controller.rb +++ b/app/controllers/guidance_groups_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Controller for the Guidances page that handles Group info class GuidanceGroupsController < ApplicationController - after_action :verify_authorized respond_to :html @@ -18,6 +18,7 @@ def admin_new end # POST /org/admin/guidancegroup/:id/admin_create + # rubocop:disable Metrics/AbcSize def admin_create # Ensure that the user can only create GuidanceGroups for their Org args = guidance_group_params.to_h.merge({ org_id: current_user.org.id }) @@ -25,13 +26,14 @@ def admin_create authorize @guidance_group if @guidance_group.save - flash.now[:notice] = success_message(@guidance_group, _("created")) + flash.now[:notice] = success_message(@guidance_group, _('created')) render :admin_edit else - flash.now[:alert] = failure_message(@guidance_group, _("create")) + flash.now[:alert] = failure_message(@guidance_group, _('create')) render :admin_new end end + # rubocop:enable Metrics/AbcSize # GET /org/admin/guidancegroup/:id/admin_edit def admin_edit @@ -40,17 +42,19 @@ def admin_edit end # PUT /org/admin/guidancegroup/:id/admin_update + # rubocop:disable Metrics/AbcSize def admin_update @guidance_group = GuidanceGroup.find(params[:id]) authorize @guidance_group if @guidance_group.update(guidance_group_params) - flash.now[:notice] = success_message(@guidance_group, _("saved")) + flash.now[:notice] = success_message(@guidance_group, _('saved')) else - flash.now[:alert] = failure_message(@guidance_group, _("save")) + flash.now[:alert] = failure_message(@guidance_group, _('save')) end render :admin_edit end + # rubocop:enable Metrics/AbcSize # PUT /org/admin/guidancegroup/:id/admin_update_publish def admin_update_publish @@ -58,10 +62,10 @@ def admin_update_publish authorize @guidance_group if @guidance_group.update(published: true) - flash[:notice] = _("Your guidance group has been published and is now available to users.") + flash[:notice] = _('Your guidance group has been published and is now available to users.') else - flash[:alert] = failure_message(@guidance_group, _("publish")) + flash[:alert] = failure_message(@guidance_group, _('publish')) end redirect_to admin_index_guidance_path end @@ -72,11 +76,9 @@ def admin_update_unpublish authorize @guidance_group if @guidance_group.update(published: false) - # rubocop:disable Layout/LineLength - flash[:notice] = _("Your guidance group is no longer published and will not be available to users.") - # rubocop:enable Layout/LineLength + flash[:notice] = _('Your guidance group is no longer published and will not be available to users.') else - flash[:alert] = failure_message(@guidance_group, _("unpublish")) + flash[:alert] = failure_message(@guidance_group, _('unpublish')) end redirect_to admin_index_guidance_path end @@ -86,9 +88,9 @@ def admin_destroy @guidance_group = GuidanceGroup.find(params[:id]) authorize @guidance_group if @guidance_group.destroy - flash[:notice] = success_message(@guidance_group, _("deleted")) + flash[:notice] = success_message(@guidance_group, _('deleted')) else - flash[:alert] = failure_message(@guidance_group, _("delete")) + flash[:alert] = failure_message(@guidance_group, _('delete')) end redirect_to admin_index_guidance_path end @@ -98,5 +100,4 @@ def admin_destroy def guidance_group_params params.require(:guidance_group).permit(:org_id, :name, :published, :optional_subset) end - end diff --git a/app/controllers/guidances_controller.rb b/app/controllers/guidances_controller.rb index 74835bd073..d3a193a12e 100644 --- a/app/controllers/guidances_controller.rb +++ b/app/controllers/guidances_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Controller for the Guidances page that handles Guidance operations class GuidancesController < ApplicationController - after_action :verify_authorized respond_to :html @@ -36,6 +36,7 @@ def admin_edit end # POST /org/admin/guidance/:id/admin_create + # rubocop:disable Metrics/AbcSize def admin_create @guidance = Guidance.new(guidance_params) authorize @guidance @@ -48,14 +49,16 @@ def admin_create guidance_group.save end end - flash.now[:notice] = success_message(@guidance, _("created")) + flash.now[:notice] = success_message(@guidance, _('created')) else - flash.now[:alert] = failure_message(@guidance, _("create")) + flash.now[:alert] = failure_message(@guidance, _('create')) end render :new_edit end + # rubocop:enable Metrics/AbcSize # PUT /org/admin/guidance/:id/admin_update + # rubocop:disable Metrics/AbcSize def admin_update @guidance = Guidance.find(params[:id]) authorize @guidance @@ -68,14 +71,16 @@ def admin_update guidance_group.save end end - flash.now[:notice] = success_message(@guidance, _("saved")) + flash.now[:notice] = success_message(@guidance, _('saved')) else - flash.now[:alert] = failure_message(@guidance, _("save")) + flash.now[:alert] = failure_message(@guidance, _('save')) end render :new_edit end + # rubocop:enable Metrics/AbcSize # DELETE /org/admin/guidance/:id/admin_destroy + # rubocop:disable Metrics/AbcSize def admin_destroy @guidance = Guidance.find(params[:id]) authorize @guidance @@ -85,46 +90,47 @@ def admin_destroy guidance_group.published = false guidance_group.save end - flash[:notice] = success_message(@guidance, _("deleted")) + flash[:notice] = success_message(@guidance, _('deleted')) else - flash[:alert] = failure_message(@guidance, _("delete")) + flash[:alert] = failure_message(@guidance, _('delete')) end redirect_to(action: :admin_index) end + # rubocop:enable Metrics/AbcSize # PUT /org/admin/guidance/:id/admin_publish + # rubocop:disable Metrics/AbcSize def admin_publish @guidance = Guidance.find(params[:id]) authorize @guidance if @guidance.update_attributes(published: true) guidance_group = GuidanceGroup.find(@guidance.guidance_group_id) - if !guidance_group.published? || guidance_group.published.nil? - guidance_group.update(published: true) - end - flash[:notice] = _("Your guidance has been published and is now available to users.") + guidance_group.update(published: true) if !guidance_group.published? || guidance_group.published.nil? + flash[:notice] = _('Your guidance has been published and is now available to users.') else - flash[:alert] = failure_message(@guidance, _("publish")) + flash[:alert] = failure_message(@guidance, _('publish')) end redirect_to(action: :admin_index) end + # rubocop:enable Metrics/AbcSize # PUT /org/admin/guidance/:id/admin_unpublish + # rubocop:disable Metrics/AbcSize def admin_unpublish @guidance = Guidance.find(params[:id]) authorize @guidance if @guidance.update_attributes(published: false) guidance_group = GuidanceGroup.find(@guidance.guidance_group_id) - unless guidance_group.guidances.where(published: true).exists? - guidance_group.update(published: false) - end - flash[:notice] = _("Your guidance is no longer published and will not be available to users.") + guidance_group.update(published: false) unless guidance_group.guidances.where(published: true).exists? + flash[:notice] = _('Your guidance is no longer published and will not be available to users.') else - flash[:alert] = failure_message(@guidance, _("unpublish")) + flash[:alert] = failure_message(@guidance, _('unpublish')) end redirect_to(action: :admin_index) end + # rubocop:enable Metrics/AbcSize private @@ -138,5 +144,4 @@ def ensure_default_group(org) GuidanceGroup.create_org_default(org) end - end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 8198b7b499..e7409d75b3 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Controller for the home page that users see when not logged in class HomeController < ApplicationController - include OrgSelectable respond_to :html @@ -14,24 +14,18 @@ class HomeController < ApplicationController # User's contact name is not filled in # Is this the desired behavior? def index - @orgs = (Org.includes(identifiers: :identifier_scheme).organisation + - Org.includes(identifiers: :identifier_scheme).institution + - Org.includes(identifiers: :identifier_scheme).default_orgs) - @orgs = @orgs.flatten.uniq.sort_by(&:name) - if user_signed_in? name = current_user.name(false) # The RolesController defaults the firstname and surname (both required fields) # to 'FirstName' and 'Surname' when a plan is shared with an unknown user - if name == "First Name Surname" + if name == 'First Name Surname' redirect_to edit_user_registration_path else redirect_to plans_url end - elsif session["devise.shibboleth_data"].present? + elsif session['devise.shibboleth_data'].present? # NOTE: Update this to handle ORCiD as well when we enable it as a login method redirect_to new_user_registration_url end end - end diff --git a/app/controllers/identifiers_controller.rb b/app/controllers/identifiers_controller.rb index 9b90f2c8a3..5d01ac4959 100644 --- a/app/controllers/identifiers_controller.rb +++ b/app/controllers/identifiers_controller.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true +# Controller that handles a user disassociating their Shib or ORCID on the profile page class IdentifiersController < ApplicationController - respond_to :html after_action :verify_authorized # DELETE /users/identifiers - # --------------------------------------------------------------------- + # rubocop:disable Metrics/AbcSize def destroy authorize Identifier user = User.find(current_user.id) @@ -15,16 +15,14 @@ def destroy # If the requested identifier belongs to the current user remove it if user.identifiers.include?(identifier) identifier.destroy! - flash[:notice] = _("Successfully unlinked your account from %{is}.") % { - is: identifier.identifier_scheme&.description - } + flash[:notice] = + format(_('Successfully unlinked your account from %{is}.'), is: identifier.identifier_scheme&.description) else - flash[:alert] = _("Unable to unlink your account from %{is}.") % { - is: identifier.identifier_scheme&.description - } + flash[:alert] = + format(_('Unable to unlink your account from %{is}.'), is: identifier.identifier_scheme&.description) end redirect_to edit_user_registration_path end - + # rubocop:enable Metrics/AbcSize end diff --git a/app/controllers/notes_controller.rb b/app/controllers/notes_controller.rb index a623a64fce..bba42c8bd8 100644 --- a/app/controllers/notes_controller.rb +++ b/app/controllers/notes_controller.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true +# Controller for the Comments section of the Write Plan page class NotesController < ApplicationController - include ConditionalUserMailer - require "pp" after_action :verify_authorized respond_to :html @@ -11,11 +10,10 @@ class NotesController < ApplicationController # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def create @note = Note.new - @note.user_id = note_params[:user_id] + # take user id from current user rather than form as form can be spoofed + @note.user_id = current_user.id # ensure user has access to plan BEFORE creating/finding answer - unless Plan.find_by(id: note_params[:plan_id]).readable_by?(@note.user_id) - raise Pundit::NotAuthorizedError - end + raise Pundit::NotAuthorizedError unless Plan.find_by(id: note_params[:plan_id]).readable_by?(@note.user_id) Answer.transaction do @answer = Answer.find_by( @@ -33,11 +31,9 @@ def create @note.answer = @answer @note.text = note_params[:text] - authorize @note @plan = @answer.plan - @question = Question.find(note_params[:question_id]) if @note.save @@ -45,31 +41,31 @@ def create answer = @note.answer plan = answer.plan owner = plan.owner - deliver_if(recipients: owner, key: "users.new_comment") do |_r| + deliver_if(recipients: owner, key: 'users.new_comment') do |_r| UserMailer.new_comment(current_user, plan, answer).deliver_now end - @notice = success_message(@note, _("created")) + @notice = success_message(@note, _('created')) render(json: { - "notes" => { - "id" => note_params[:question_id], - "html" => render_to_string(partial: "layout", locals: { + 'notes' => { + 'id' => note_params[:question_id], + 'html' => render_to_string(partial: 'layout', locals: { plan: @plan, question: @question, answer: @answer }, formats: [:html]) }, - "title" => { - "id" => note_params[:question_id], - "html" => render_to_string(partial: "title", locals: { + 'title' => { + 'id' => note_params[:question_id], + 'html' => render_to_string(partial: 'title', locals: { answer: @answer }, formats: [:html]) } }.to_json, status: :created) else @status = false - @notice = failure_message(@note, _("create")) + @notice = failure_message(@note, _('create')) render json: { - "msg" => @notice + 'msg' => @notice }.to_json, status: :bad_request end end @@ -89,27 +85,27 @@ def update question_id = @note.answer.question_id.to_s if @note.update(note_params) - @notice = success_message(@note, _("saved")) + @notice = success_message(@note, _('saved')) render(json: { - "notes" => { - "id" => question_id, - "html" => render_to_string(partial: "layout", locals: { + 'notes' => { + 'id' => question_id, + 'html' => render_to_string(partial: 'layout', locals: { plan: @plan, question: @question, answer: @answer }, formats: [:html]) }, - "title" => { - "id" => question_id, - "html" => render_to_string(partial: "title", locals: { + 'title' => { + 'id' => question_id, + 'html' => render_to_string(partial: 'title', locals: { answer: @answer }, formats: [:html]) } }.to_json, status: :ok) else - @notice = failure_message(@note, _("save")) + @notice = failure_message(@note, _('save')) render json: { - "msg" => @notice + 'msg' => @notice }.to_json, status: :bad_request end end @@ -131,27 +127,27 @@ def archive question_id = @note.answer.question_id.to_s if @note.update(note_params) - @notice = success_message(@note, _("removed")) + @notice = success_message(@note, _('removed')) render(json: { - "notes" => { - "id" => question_id, - "html" => render_to_string(partial: "layout", locals: { + 'notes' => { + 'id' => question_id, + 'html' => render_to_string(partial: 'layout', locals: { plan: @plan, question: @question, answer: @answer }, formats: [:html]) }, - "title" => { - "id" => question_id, - "html" => render_to_string(partial: "title", locals: { + 'title' => { + 'id' => question_id, + 'html' => render_to_string(partial: 'title', locals: { answer: @answer }, formats: [:html]) } }.to_json, status: :ok) else - @notice = failure_message(@note, _("remove")) + @notice = failure_message(@note, _('remove')) render json: { - "msg" => @notice + 'msg' => @notice }.to_json, status: :bad_request end end @@ -164,5 +160,4 @@ def note_params .permit(:text, :archived_by, :user_id, :answer_id, :plan_id, :question_id) end - end diff --git a/app/controllers/org_admin/conditions_controller.rb b/app/controllers/org_admin/conditions_controller.rb index 8c2c59376d..552ade2114 100644 --- a/app/controllers/org_admin/conditions_controller.rb +++ b/app/controllers/org_admin/conditions_controller.rb @@ -1,35 +1,36 @@ # frozen_string_literal: true -class OrgAdmin::ConditionsController < ApplicationController +module OrgAdmin + # Controller that handles conditional questions + class ConditionsController < ApplicationController + # /org_admin/questions/:question_id/conditions/new + def new + question = Question.find(params[:question_id]) + condition_no = new_condition_params[:condition_no] + next_condition_no = condition_no.to_i + 1 + render json: { add_link: render_to_string(partial: 'add', + formats: :html, + layout: false, + locals: { question: question, + condition_no: next_condition_no }), + attachment_partial: render_to_string(partial: 'form', + formats: :html, + layout: false, + locals: { + question: question, + cond: Condition.new(question: question), + condition_no: condition_no + }) } + end - # /org_admin/questions/:question_id/conditions/new - def new - question = Question.find(params[:question_id]) - condition_no = new_condition_params[:condition_no] - next_condition_no = condition_no.to_i + 1 - render json: { add_link: render_to_string(partial: "add", - formats: :html, - layout: false, - locals: { question: question, - condition_no: next_condition_no }), - attachment_partial: render_to_string(partial: "form", - formats: :html, - layout: false, - locals: { - question: question, - cond: Condition.new(question: question), - condition_no: condition_no - }) } - end - - private + private - def new_condition_params - params.permit(:condition_no) - end + def new_condition_params + params.permit(:condition_no) + end - def condition_params - params.require(:question_option_id, :action_type).permit(:remove_question_id, :condition_no) + def condition_params + params.require(:question_option_id, :action_type).permit(:remove_question_id, :condition_no) + end end - end diff --git a/app/controllers/org_admin/departments_controller.rb b/app/controllers/org_admin/departments_controller.rb index 8d52cf9293..b77ae38503 100644 --- a/app/controllers/org_admin/departments_controller.rb +++ b/app/controllers/org_admin/departments_controller.rb @@ -1,80 +1,83 @@ # frozen_string_literal: true -class OrgAdmin::DepartmentsController < ApplicationController +module OrgAdmin + # Controller that handles department operations + class DepartmentsController < ApplicationController + after_action :verify_authorized + respond_to :html - after_action :verify_authorized - respond_to :html - - # GET add new department - def new - @department = Department.new - @org_id = org_id - @department.org_id = @org_id - authorize @department - end + # GET add new department + def new + @department = Department.new + @org_id = org_id + @department.org_id = @org_id + authorize @department + end - # POST /departments - # POST /departments.json - def create - @department = Department.new(department_params) - @org_id = org_id + # POST /departments + # POST /departments.json + def create + @department = Department.new(department_params) + @org_id = org_id - authorize @department + authorize @department - if @department.save - flash.now[:notice] = success_message(@department, _("created")) - # reset value - @department = nil - else - flash.now[:alert] = failure_message(@department, _("create")) + if @department.save + flash.now[:notice] = success_message(@department, _('created')) + # reset value + @department = nil + else + flash.now[:alert] = failure_message(@department, _('create')) + end + render :new end - render :new - end - # GET /departments/1/edit - def edit - @department = Department.find(params[:id]) - @org_id = org_id - authorize @department - end + # GET /departments/1/edit + def edit + @department = Department.find(params[:id]) + @org_id = org_id + authorize @department + end - # PUT /departments/1 - def update - @department = Department.find(params[:id]) - @org_id = org_id - authorize @department + # PUT /departments/1 + # rubocop:disable Metrics/AbcSize + def update + @department = Department.find(params[:id]) + @org_id = org_id + authorize @department - if @department.update(department_params) - flash.now[:notice] = success_message(@department, _("saved")) - else - flash.now[:alert] = failure_message(@department, _("save")) + if @department.update(department_params) + flash.now[:notice] = success_message(@department, _('saved')) + else + flash.now[:alert] = failure_message(@department, _('save')) + end + render :edit end - render :edit - end + # rubocop:enable Metrics/AbcSize - # DELETE /departments/1 - def destroy - @department = Department.find(params[:id]) - @org_id = org_id - authorize @department - url = "#{admin_edit_org_path(@org_id)}\#departments" + # DELETE /departments/1 + def destroy + @department = Department.find(params[:id]) + @org_id = org_id + authorize @department + url = "#{admin_edit_org_path(@org_id)}#departments" - if @department.destroy - flash[:notice] = success_message(@department, _("deleted")) - else - flash[:alert] = failure_message(@department, _("delete")) + if @department.destroy + flash[:notice] = success_message(@department, _('deleted')) + else + flash[:alert] = failure_message(@department, _('delete')) + end + redirect_to url end - redirect_to url - end - private + private - def department_params - params.require(:department).permit(:id, :name, :code, :org_id) - end + def department_params + params.require(:department).permit(:id, :name, :code, :org_id) + end - def org_id - current_user.can_super_admin? ? params[:org_id] : current_user.org_id + def org_id + current_user.can_super_admin? ? params[:org_id] : current_user.org_id + end end - end diff --git a/app/controllers/org_admin/phase_versions_controller.rb b/app/controllers/org_admin/phase_versions_controller.rb index 1e0c34d6ba..d3081d2b14 100644 --- a/app/controllers/org_admin/phase_versions_controller.rb +++ b/app/controllers/org_admin/phase_versions_controller.rb @@ -1,20 +1,21 @@ # frozen_string_literal: true -class OrgAdmin::PhaseVersionsController < ApplicationController +module OrgAdmin + # Controller that handles creating new versions of Phases + class PhaseVersionsController < ApplicationController + include Versionable - include Versionable - - # POST /org_admin/templates/:template_id/phases/:phase_id/versions - def create - @phase = Phase.find(params[:phase_id]) - authorize @phase, :create? - @new_phase = get_modifiable(@phase) - flash[:notice] = if @new_phase == @phase - _("This template is already a draft") - else - _("New version of Template created") - end - redirect_to org_admin_template_phase_url(@new_phase.template, @new_phase) + # POST /org_admin/templates/:template_id/phases/:phase_id/versions + def create + @phase = Phase.find(params[:phase_id]) + authorize @phase + @new_phase = get_modifiable(@phase) + flash[:notice] = if @new_phase == @phase + _('This template is already a draft') + else + _('New version of Template created') + end + redirect_to org_admin_template_phase_url(@new_phase.template, @new_phase) + end end - end diff --git a/app/controllers/org_admin/phases_controller.rb b/app/controllers/org_admin/phases_controller.rb index 84ef0134ca..f27a3e7380 100644 --- a/app/controllers/org_admin/phases_controller.rb +++ b/app/controllers/org_admin/phases_controller.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true module OrgAdmin - + # Controller that handles phases class PhasesController < ApplicationController - include Versionable after_action :verify_authorized @@ -15,7 +14,7 @@ def show authorize phase unless phase.template.latest? # rubocop:disable Layout/LineLength - flash[:notice] = _("You are viewing a historical version of this template. You will not be able to make changes.") + flash[:notice] = _('You are viewing a historical version of this template. You will not be able to make changes.') # rubocop:enable Layout/LineLength end sections = if phase.template.customization_of? && phase.template.latest? @@ -26,9 +25,9 @@ def show # will be readonly phase.sections.order(:number) end - render("container", + render('container', locals: { - partial_path: "show", + partial_path: 'show', template: phase.template, phase: phase, prefix_section: phase.prefix_section, @@ -40,7 +39,7 @@ def show # rubocop:enable Metrics/AbcSize # GET /org_admin/templates/:template_id/phases/:id/edit - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def edit phase = Phase.includes(:template).find(params[:id]) authorize phase @@ -52,9 +51,9 @@ def edit section: params[:section] ) else - render("container", + render('container', locals: { - partial_path: "edit", + partial_path: 'edit', template: phase.template, phase: phase, prefix_section: phase.prefix_section, @@ -65,7 +64,7 @@ def edit }) end end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength # preview a phase # GET /org_admin/templates/:template_id/phases/:id/preview @@ -78,6 +77,7 @@ def preview # add a new phase to a passed template # GET /org_admin/templates/:template_id/phases/new + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def new template = Template.includes(:phases).find(params[:template_id]) if template.latest? @@ -93,22 +93,23 @@ def new else org_admin_templates_path end - render("/org_admin/templates/container", + render('/org_admin/templates/container', locals: { - partial_path: "new", + partial_path: 'new', template: template, phase: phase, referrer: local_referrer }) else render org_admin_templates_path, - alert: _("You cannot add a phase to a historical version of a template.") + alert: _('You cannot add a phase to a historical version of a template.') end end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength # create a phase # POST /org_admin/templates/:template_id/phases - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def create phase = Phase.new(phase_params) phase.template = Template.find(params[:template_id]) @@ -117,12 +118,13 @@ def create phase = get_new(phase) phase.modifiable = true if phase.save - flash[:notice] = success_message(phase, _("created")) + flash[:notice] = success_message(phase, _('created')) else - flash[:alert] = failure_message(phase, _("create")) + flash[:alert] = failure_message(phase, _('create')) end rescue StandardError => e - flash[:alert] = _("Unable to create a new version of this template.") + "
    " + e.message + msg = _('Unable to create a new version of this template.
    ') + flash[:alert] = "#{msg}
    #{e.message}" end if flash[:alert].present? redirect_to new_org_admin_template_phase_path(template_id: phase.template.id) @@ -131,26 +133,29 @@ def create id: phase.id) end end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength # update a phase of a template # PUT /org_admin/templates/:template_id/phases/:id + # rubocop:disable Metrics/AbcSize def update phase = Phase.find(params[:id]) authorize phase begin phase = get_modifiable(phase) if phase.update(phase_params) - flash[:notice] = success_message(phase, _("updated")) + flash[:notice] = success_message(phase, _('updated')) else - flash[:alert] = failure_message(phase, _("update")) + flash[:alert] = failure_message(phase, _('update')) end rescue StandardError => e - flash[:alert] = _("Unable to create a new version of this template.") + "
    " + e.message + msg = _('Unable to create a new version of this template.') + flash[:alert] = "#{msg}
    #{e.message}" end redirect_to edit_org_admin_template_phase_path(template_id: phase.template.id, id: phase.id) end + # rubocop:enable Metrics/AbcSize # POST /org_admin/templates/:template_id/phases/:id/sort def sort @@ -171,12 +176,13 @@ def destroy phase = get_modifiable(phase) template = phase.template if phase.destroy! - flash[:notice] = success_message(phase, _("deleted")) + flash[:notice] = success_message(phase, _('deleted')) else - flash[:alert] = failure_message(phase, _("delete")) + flash[:alert] = failure_message(phase, _('delete')) end rescue StandardError => e - flash[:alert] = _("Unable to create a new version of this template.") + "
    " + e.message + msg = _('Unable to create a new version of this template.') + flash[:alert] = "#{msg}
    #{e.message}" end if flash[:alert].present? @@ -192,7 +198,5 @@ def destroy def phase_params params.require(:phase).permit(:title, :description, :number, sort_order: []) end - end - -end \ No newline at end of file +end diff --git a/app/controllers/org_admin/plans_controller.rb b/app/controllers/org_admin/plans_controller.rb index d04739b5fc..7537f39ff7 100644 --- a/app/controllers/org_admin/plans_controller.rb +++ b/app/controllers/org_admin/plans_controller.rb @@ -1,83 +1,86 @@ # frozen_string_literal: true -class OrgAdmin::PlansController < ApplicationController +module OrgAdmin + # Controller that handles admin operations for plans + class PlansController < ApplicationController + # GET org_admin/plans + # rubocop:disable Metrics/AbcSize + def index + # Test auth directly and throw Pundit error sincePundit + # is unaware of namespacing + raise Pundit::NotAuthorizedError unless current_user.present? && current_user.can_org_admin? - # GET org_admin/plans - def index - # Test auth directly and throw Pundit error sincePundit - # is unaware of namespacing - raise Pundit::NotAuthorizedError unless current_user.present? && current_user.can_org_admin? + sql = 'users.org_id = ? AND plans.feedback_requested is TRUE AND roles.active is TRUE' + feedback_ids = Role.creator.joins(:user, :plan) + .where(sql, current_user.org_id).pluck(:plan_id) + @feedback_plans = Plan.where(id: feedback_ids).compact - sql = "users.org_id = ? AND plans.feedback_requested is TRUE AND roles.active is TRUE" - feedback_ids = Role.creator.joins(:user, :plan) - .where(sql, current_user.org_id).pluck(:plan_id) - @feedback_plans = Plan.where(id: feedback_ids).reject(&:nil?) - - @super_admin = current_user.can_super_admin? - @clicked_through = params[:click_through].present? - @plans = @super_admin ? Plan.all.page(1) : current_user.org.org_admin_plans.page(1) - end + @super_admin = current_user.can_super_admin? + @clicked_through = params[:click_through].present? + @plans = @super_admin ? Plan.all.page(1) : current_user.org.org_admin_plans.page(1) + end + # rubocop:enable Metrics/AbcSize - # GET org_admin/plans/:id/feedback_complete - def feedback_complete - plan = Plan.find(params[:id]) - # Test auth directly and throw Pundit error sincePundit is - # unaware of namespacing - raise Pundit::NotAuthorizedError unless current_user.present? && current_user.can_org_admin? - raise Pundit::NotAuthorizedError unless plan.reviewable_by?(current_user.id) + # GET org_admin/plans/:id/feedback_complete + # rubocop:disable Metrics/AbcSize + def feedback_complete + plan = Plan.find(params[:id]) + # Test auth directly and throw Pundit error sincePundit is + # unaware of namespacing + raise Pundit::NotAuthorizedError unless current_user.present? && current_user.can_org_admin? + raise Pundit::NotAuthorizedError unless plan.reviewable_by?(current_user.id) - if plan.complete_feedback(current_user) - # rubocop:disable Layout/LineLength - redirect_to(org_admin_plans_path, - notice: _("%{plan_owner} has been notified that you have finished providing feedback") % { - plan_owner: plan.owner.name(false) - }) - # rubocop:enable Layout/LineLength - else - redirect_to org_admin_plans_path, - alert: _("Unable to notify user that you have finished providing feedback.") + if plan.complete_feedback(current_user) + # rubocop:disable Layout/LineLength + redirect_to(org_admin_plans_path, + notice: format(_('%{plan_owner} has been notified that you have finished providing feedback'), plan_owner: plan.owner.name(false))) + # rubocop:enable Layout/LineLength + else + redirect_to org_admin_plans_path, + alert: _('Unable to notify user that you have finished providing feedback.') + end end - end + # rubocop:enable Metrics/AbcSize - # GET /org_admin/download_plans - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - def download_plans - # Test auth directly and throw Pundit error sincePundit - # is unaware of namespacing - raise Pundit::NotAuthorizedError unless current_user.present? && current_user.can_org_admin? + # GET /org_admin/download_plans + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def download_plans + # Test auth directly and throw Pundit error sincePundit + # is unaware of namespacing + raise Pundit::NotAuthorizedError unless current_user.present? && current_user.can_org_admin? - org = current_user.org - file_name = org.name.gsub(/ /, "_") - .gsub(/[.;,]/, "") - header_cols = [ - _("Project title").to_s, - _("Template").to_s, - _("Organisation").to_s, - _("Owner name").to_s, - _("Owner email").to_s, - _("Updated").to_s, - _("Visibility").to_s - ] + org = current_user.org + file_name = org.name.gsub(/ /, '_') + .gsub(/[.;,]/, '') + header_cols = [ + _('Project title').to_s, + _('Template').to_s, + _('Organisation').to_s, + _('Owner name').to_s, + _('Owner email').to_s, + _('Updated').to_s, + _('Visibility').to_s + ] - plans = CSV.generate do |csv| - csv << header_cols - org.org_admin_plans.includes(template: :org).order(updated_at: :desc).each do |plan| - csv << [ - plan.title.to_s, - plan.template.title.to_s, - (plan.owner.org.present? ? plan.owner.org.name : "").to_s, - plan.owner.name(false).to_s, - plan.owner.email.to_s, - l(plan.latest_update.to_date, format: :csv).to_s, - Plan::VISIBILITY_MESSAGE[plan.visibility.to_sym].capitalize.to_s - ] + plans = CSV.generate do |csv| + csv << header_cols + org.org_admin_plans.includes(template: :org).order(updated_at: :desc).each do |plan| + csv << [ + plan.title.to_s, + plan.template.title.to_s, + (plan.owner&.org&.present? ? plan.owner.org.name : '').to_s, + plan.owner&.name(false)&.to_s, + plan.owner&.email&.to_s, + l(plan.latest_update.to_date, format: :csv).to_s, + Plan::VISIBILITY_MESSAGE[plan.visibility.to_sym].capitalize.to_s + ] + end end - end - respond_to do |format| - format.csv { send_data plans, filename: "#{file_name}.csv" } + respond_to do |format| + format.csv { send_data plans, filename: "#{file_name}.csv" } + end end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - end diff --git a/app/controllers/org_admin/question_options_controller.rb b/app/controllers/org_admin/question_options_controller.rb index 1a5e1948ae..0bdfc7e8d2 100644 --- a/app/controllers/org_admin/question_options_controller.rb +++ b/app/controllers/org_admin/question_options_controller.rb @@ -1,14 +1,13 @@ # frozen_string_literal: true module OrgAdmin - + # Controller that handles question options class QuestionOptionsController < ApplicationController - include Versionable after_action :verify_authorized - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def destroy question_option = QuestionOption.find(params[:id]) option_id_to_remove = question_option.id.to_s @@ -22,12 +21,12 @@ def destroy question.conditions.each do |cond| cond.destroy if cond.option_list.include?(option_id_to_remove) end - flash[:notice] = success_message(question_option, _("deleted")) + flash[:notice] = success_message(question_option, _('deleted')) else - flash[:alert] = flash[:alert] = failure_message(question_option, _("delete")) + flash[:alert] = flash[:alert] = failure_message(question_option, _('delete')) end rescue StandardError - flash[:alert] = _("Unable to create a new version of this template.") + flash[:alert] = _('Unable to create a new version of this template.') end redirect_to edit_org_admin_template_phase_path( template_id: section.phase.template.id, @@ -35,8 +34,6 @@ def destroy section: section.id ) end - # rubocop:enable Metrics/AbcSize - + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength end - end diff --git a/app/controllers/org_admin/questions_controller.rb b/app/controllers/org_admin/questions_controller.rb index f3061c764e..4e789df405 100644 --- a/app/controllers/org_admin/questions_controller.rb +++ b/app/controllers/org_admin/questions_controller.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true module OrgAdmin - + # Controller that handles questions class QuestionsController < ApplicationController - include AllowedQuestionFormats include Versionable include ConditionsHelper @@ -18,7 +17,7 @@ def show section: { phase: :template }) .find(params[:id]) authorize question - render json: { html: render_to_string(partial: "show", locals: { + render json: { html: render_to_string(partial: 'show', locals: { template: question.section.phase.template, section: question.section, question: question, @@ -31,7 +30,7 @@ def show def open_conditions question = Question.find(params[:question_id]) authorize question - render json: { container: render_to_string(partial: "org_admin/conditions/container", + render json: { container: render_to_string(partial: 'org_admin/conditions/container', formats: :html, layout: false, locals: { @@ -41,16 +40,14 @@ def open_conditions webhooks: webhook_hash(question.conditions) } end - # rubocop:disable Layout/LineLength # GET /org_admin/templates/[:template_id]/phases/[:phase_id]/sections/[:id]/questions/[:question_id]/edit - # rubocop:enable Layout/LineLength def edit question = Question.includes(:annotations, :question_options, section: { phase: :template }) .find(params[:id]) authorize question - render json: { html: render_to_string(partial: "edit", locals: { + render json: { html: render_to_string(partial: 'edit', locals: { template: question.section.phase.template, section: question.section, question: question, @@ -60,20 +57,21 @@ def edit end # GET /org_admin/templates/:template_id/phases/:phase_id/sections/:section_id/questions/new + # rubocop:disable Metrics/AbcSize def new section = Section.includes(:questions, phase: :template).find(params[:section_id]) nbr = section.questions.maximum(:number) - question_format = QuestionFormat.find_by(title: "Text area") + question_format = QuestionFormat.find_by(title: 'Text area') question = Question.new(section_id: section.id, question_format: question_format, number: nbr.present? ? nbr + 1 : 1) question_formats = allowed_question_formats authorize question - render json: { html: render_to_string(partial: "form", locals: { + render json: { html: render_to_string(partial: 'form', locals: { template: section.phase.template, section: section, question: question, - method: "post", + method: 'post', url: org_admin_template_phase_section_questions_path( template_id: section.phase.template.id, phase_id: section.phase.id, @@ -82,6 +80,7 @@ def new question_formats: question_formats }) } end + # rubocop:enable Metrics/AbcSize # POST /org_admin/templates/:template_id/phases/:phase_id/sections/:section_id/questions # rubocop:disable Metrics/AbcSize @@ -92,12 +91,12 @@ def create question = get_new(question) section = question.section if question.save - flash[:notice] = success_message(question, _("created")) + flash[:notice] = success_message(question, _('created')) else - flash[:alert] = failure_message(question, _("create")) + flash[:alert] = failure_message(question, _('create')) end rescue StandardError - flash[:alert] = _("Unable to create a new version of this template.") + flash[:alert] = _('Unable to create a new version of this template.') end redirect_to edit_org_admin_template_phase_path( template_id: section.phase.template.id, @@ -109,6 +108,7 @@ def create # PUT /org_admin/templates/:template_id/phases/:phase_id/sections/:section_id/questions/:id # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def update question = Question.find(params[:id]) authorize question @@ -151,7 +151,7 @@ def update # version of the question # and also rewrite the remove_data question ids attrs = question_params - attrs = update_option_ids(attrs, old_to_new_opts) if new_version + attrs = update_option_ids(attrs, old_to_new_opts) if new_version && !attrs['question_options_attributes'].nil? # Need to reattach the incoming annotation's and question_options to the # modifiable (versioned) question @@ -161,12 +161,12 @@ def update # add check for number present to ensure this is not just an annotation attrs[:theme_ids] = [] if attrs[:theme_ids].blank? && attrs[:number].present? if question.update(attrs) - if question.update_conditions(sanitize_hash(params["conditions"]), + if question.update_conditions(sanitize_hash(params['conditions']), old_to_new_opts, question_id_map) - flash[:notice] = success_message(question, _("updated")) + flash[:notice] = success_message(question, _('updated')) end else - flash[:alert] = flash[:alert] = failure_message(question, _("update")) + flash[:alert] = flash[:alert] = failure_message(question, _('update')) end if question.section.phase.template.customization_of.present? redirect_to org_admin_template_phase_path( @@ -183,7 +183,7 @@ def update end end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # DELETE /org_admin/templates/:template_id/phases/:phase_id/sections/:section_id/questions/:id # rubocop:disable Metrics/AbcSize @@ -194,12 +194,12 @@ def destroy question = get_modifiable(question) section = question.section if question.destroy! - flash[:notice] = success_message(question, _("deleted")) + flash[:notice] = success_message(question, _('deleted')) else - flash[:alert] = flash[:alert] = failure_message(question, _("delete")) + flash[:alert] = flash[:alert] = failure_message(question, _('delete')) end rescue StandardError - flash[:alert] = _("Unable to create a new version of this template.") + flash[:alert] = _('Unable to create a new version of this template.') end redirect_to edit_org_admin_template_phase_path( template_id: section.phase.template.id, @@ -231,7 +231,7 @@ def sanitize_hash(param_conditions) hash_of_hashes.each do |cond_name, cond_hash| sanitized_hash = {} cond_hash.each do |k, v| - v = ActionController::Base.helpers.sanitize(v) if k.start_with?("webhook") + v = ActionController::Base.helpers.sanitize(v) if k.start_with?('webhook') sanitized_hash[k] = v end res[cond_name] = sanitized_hash @@ -256,11 +256,11 @@ def question_params # options are now out of sync with the params. # This sorts that out. def update_option_ids(attrs_in, opt_map) - qopts = attrs_in["question_options_attributes"] + qopts = attrs_in['question_options_attributes'] qopts.each_pair do |_, attr_hash| - old_id = attr_hash["id"] + old_id = attr_hash['id'] new_id = opt_map[old_id] - attr_hash["id"] = new_id + attr_hash['id'] = new_id end attrs_in end @@ -268,6 +268,7 @@ def update_option_ids(attrs_in, opt_map) # When a template gets versioned by changes to one of its questions we need to loop # through the incoming params and ensure that the annotations and question_options # get attached to the new question + # rubocop:disable Metrics/AbcSize def transfer_associations(attrs, question) if attrs[:annotations_attributes].present? attrs[:annotations_attributes].each_pair do |_, value| @@ -280,7 +281,6 @@ def transfer_associations(attrs, question) end attrs end - + # rubocop:enable Metrics/AbcSize end - end diff --git a/app/controllers/org_admin/sections_controller.rb b/app/controllers/org_admin/sections_controller.rb index 415d5139c4..2b8c74a698 100644 --- a/app/controllers/org_admin/sections_controller.rb +++ b/app/controllers/org_admin/sections_controller.rb @@ -1,22 +1,22 @@ # frozen_string_literal: true module OrgAdmin - + # Controller that handles sections class SectionsController < ApplicationController - include Versionable respond_to :html after_action :verify_authorized # GET /org_admin/templates/[:template_id]/phases/[:phase_id]/sections + # rubocop:disable Metrics/AbcSize def index authorize Section.new phase = Phase.includes(:template, :sections).find(params[:phase_id]) edit = phase.template.latest? && (current_user.can_modify_templates? && (phase.template.org_id == current_user.org_id)) - render partial: "index", + render partial: 'index', locals: { template: phase.template, phase: phase, @@ -28,6 +28,7 @@ def index edit: edit } end + # rubocop:enable Metrics/AbcSize # GET /org_admin/templates/[:template_id]/phases/[:phase_id]/sections/[:id] def show @@ -36,7 +37,7 @@ def show @section = Section.includes(questions: %i[annotations question_options]) .find(params[:id]) @template = Template.find(params[:template_id]) - render json: { html: render_to_string(partial: "show", + render json: { html: render_to_string(partial: 'show', locals: { template: @template, section: @section }) } end @@ -49,9 +50,9 @@ def edit # User cannot edit a section if its not modifiable or the template is not the # latest redirect to show partial_name = if section.modifiable? && section.phase.template.latest? - "edit" + 'edit' else - "show" + 'show' end render json: { html: render_to_string(partial: partial_name, locals: { @@ -62,12 +63,12 @@ def edit end # POST /org_admin/templates/[:template_id]/phases/[:phase_id]/sections - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def create @phase = Phase.find_by(id: params[:phase_id]) if @phase.nil? flash[:alert] = - _("Unable to create a new section. The phase you specified does not exist.") + _('Unable to create a new section. The phase you specified does not exist.') redirect_to edit_org_admin_template_path(template_id: params[:template_id]) return end @@ -75,21 +76,21 @@ def create authorize @section @section = get_new(@section) if @section.save - flash[:notice] = success_message(@section, _("created")) + flash[:notice] = success_message(@section, _('created')) redirect_to edit_org_admin_template_phase_path( id: @section.phase_id, template_id: @phase.template_id, section: @section.id ) else - flash[:alert] = failure_message(@section, _("create")) + flash[:alert] = failure_message(@section, _('create')) redirect_to edit_org_admin_template_phase_path( template_id: @phase.template_id, id: @section.phase_id ) end end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength # PUT /org_admin/templates/[:template_id]/phases/[:phase_id]/sections/[:id] # rubocop:disable Metrics/AbcSize @@ -99,12 +100,13 @@ def update begin section = get_modifiable(section) if section.update(section_params) - flash[:notice] = success_message(section, _("saved")) + flash[:notice] = success_message(section, _('saved')) else - flash[:alert] = failure_message(section, _("save")) + flash[:alert] = failure_message(section, _('save')) end rescue StandardError => e - flash[:alert] = _("Unable to create a new version of this template.") + "
    " + e.message + msg = _('Unable to create a new version of this template.') + flash[:alert] = "#{msg}
    #{e.message}" end redirect_to edit_org_admin_template_phase_path( @@ -123,12 +125,13 @@ def destroy section = get_modifiable(section) phase = section.phase if section.destroy! - flash[:notice] = success_message(section, _("deleted")) + flash[:notice] = success_message(section, _('deleted')) else - flash[:alert] = failure_message(section, _("delete")) + flash[:alert] = failure_message(section, _('delete')) end rescue StandardError => e - flash[:alert] = _("Unable to create a new version of this template.") + "
    " + e.message + msg = _('Unable to delete this version of the template.') + flash[:alert] = "#{msg}
    #{e.message}" end redirect_to(edit_org_admin_template_phase_path( @@ -143,7 +146,5 @@ def destroy def section_params params.require(:section).permit(:title, :description) end - end - -end \ No newline at end of file +end diff --git a/app/controllers/org_admin/template_copies_controller.rb b/app/controllers/org_admin/template_copies_controller.rb index 0d6d989d32..6e3c99af85 100644 --- a/app/controllers/org_admin/template_copies_controller.rb +++ b/app/controllers/org_admin/template_copies_controller.rb @@ -1,27 +1,30 @@ # frozen_string_literal: true -class OrgAdmin::TemplateCopiesController < ApplicationController +module OrgAdmin + # Controller that handles copying templates + class TemplateCopiesController < ApplicationController + include TemplateMethods - include TemplateMethods + after_action :verify_authorized - after_action :verify_authorized - - # POST /org_admin/templates/:id/copy (AJAX) - def create - @template = Template.find(params[:template_id]) - authorize @template, :copy? - begin - new_copy = @template.generate_copy!(current_user.org) - flash[:notice] = _("%{template_type} was successfully copied.") % {template_type: template_type(@template).capitalize} - redirect_to edit_org_admin_template_path(new_copy) - rescue StandardError - flash[:alert] = failure_message(_("copy"), template_type(@template)) - if request.referrer.present? - redirect_back(fallback_location: org_admin_templates_path) - else - redirect_to org_admin_templates_path + # POST /org_admin/templates/:id/copy (AJAX) + # rubocop:disable Metrics/AbcSize + def create + @template = Template.find(params[:template_id]) + authorize @template, :copy? + begin + new_copy = @template.generate_copy!(current_user.org) + flash[:notice] = "#{template_type(@template).capitalize} was successfully copied." + redirect_to edit_org_admin_template_path(new_copy) + rescue StandardError + flash[:alert] = failure_message(_('copy'), template_type(@template)) + if request.referrer.present? + redirect_back(fallback_location: org_admin_templates_path) + else + redirect_to org_admin_templates_path + end end end + # rubocop:enable Metrics/AbcSize end - end diff --git a/app/controllers/org_admin/template_customization_transfers_controller.rb b/app/controllers/org_admin/template_customization_transfers_controller.rb index 5268b2bcf0..f0b98e28b7 100644 --- a/app/controllers/org_admin/template_customization_transfers_controller.rb +++ b/app/controllers/org_admin/template_customization_transfers_controller.rb @@ -1,32 +1,33 @@ # frozen_string_literal: true -class OrgAdmin::TemplateCustomizationTransfersController < ApplicationController - - include Versionable - - after_action :verify_authorized - - # POST /org_admin/templates/:id/transfer_customization - # - # The funder template's id is passed through here - def create - @template = Template.find(params[:template_id]) - authorize @template, :transfer_customization? - if @template.upgrade_customization? - # If the customized template is not published it will not version, so publish it! - previously_published = @template.published? - @template.publish unless previously_published - - @new_customization = @template.upgrade_customization! - - # Reset the published flag if the customized template was not previously published - @template.update(published: false) unless previously_published - - redirect_to org_admin_template_path(@new_customization) - else - flash[:alert] = _("That template is no longer customizable.") - redirect_back(fallback_location: org_admin_templates_path) +module OrgAdmin + # Controller that handles transfering parent template changes to a customized template + class TemplateCustomizationTransfersController < ApplicationController + include Versionable + + after_action :verify_authorized + + # POST /org_admin/templates/:id/transfer_customization + # + # The funder template's id is passed through here + def create + @template = Template.find(params[:template_id]) + authorize @template, :transfer_customization? + if @template.upgrade_customization? + # If the customized template is not published it will not version, so publish it! + previously_published = @template.published? + @template.publish unless previously_published + + @new_customization = @template.upgrade_customization! + + # Reset the published flag if the customized template was not previously published + @template.update(published: false) unless previously_published + + redirect_to org_admin_template_path(@new_customization) + else + flash[:alert] = _('That template is no longer customizable.') + redirect_back(fallback_location: org_admin_templates_path) + end end end - end diff --git a/app/controllers/org_admin/template_customizations_controller.rb b/app/controllers/org_admin/template_customizations_controller.rb index 91094a5ead..15c37f533b 100644 --- a/app/controllers/org_admin/template_customizations_controller.rb +++ b/app/controllers/org_admin/template_customizations_controller.rb @@ -1,27 +1,30 @@ # frozen_string_literal: true -class OrgAdmin::TemplateCustomizationsController < ApplicationController +module OrgAdmin + # Controller that handles customizing a template + class TemplateCustomizationsController < ApplicationController + include Paginable + include Versionable + after_action :verify_authorized - include Paginable - include Versionable - after_action :verify_authorized - - # POST /org_admin/templates/:id/customize - def create - @template = Template.find(params[:template_id]) - authorize(@template, :customize?) - if @template.customize?(current_user.org) - begin - @customisation = @template.customize!(current_user.org) - redirect_to org_admin_template_path(@customisation) - return - rescue ArgumentError - flash[:alert] = _("Unable to customize that template.") + # POST /org_admin/templates/:id/customize + # rubocop:disable Metrics/AbcSize + def create + @template = Template.find(params[:template_id]) + authorize(@template, :customize?) + if @template.customize?(current_user.org) + begin + @customisation = @template.customize!(current_user.org) + redirect_to org_admin_template_path(@customisation) + return + rescue ArgumentError + flash[:alert] = _('Unable to customize that template.') + end + else + flash[:notice] = _('That template is not customizable.') end - else - flash[:notice] = _("That template is not customizable.") + redirect_back(fallback_location: org_admin_templates_path) end - redirect_back(fallback_location: org_admin_templates_path) + # rubocop:enable Metrics/AbcSize end - end diff --git a/app/controllers/org_admin/templates_controller.rb b/app/controllers/org_admin/templates_controller.rb index 16e8ef1865..537ea4c5e4 100644 --- a/app/controllers/org_admin/templates_controller.rb +++ b/app/controllers/org_admin/templates_controller.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true module OrgAdmin - - # rubocop:disable Metrics/ClassLength + # Controller that handles templates class TemplatesController < ApplicationController - include Paginable include Versionable include TemplateMethods @@ -13,16 +11,16 @@ class TemplatesController < ApplicationController # The root version of index which returns all templates # GET /org_admin/templates - # ----------------------------------------------------- + # rubocop:disable Metrics/AbcSize def index authorize Template templates = Template.latest_version.where(customization_of: nil) published = templates.select { |t| t.published? || t.draft? }.length @orgs = Org.managed - @title = _("All Templates") + @title = _('All Templates') @templates = templates.includes(:org).page(1) - @query_params = { sort_field: "templates.title", sort_direction: "asc" } + @query_params = { sort_field: 'templates.title', sort_direction: 'asc' } @all_count = templates.length @published_count = published.present? ? published : 0 @unpublished_count = if published.present? @@ -32,11 +30,12 @@ def index end render :index end + # rubocop:enable Metrics/AbcSize # A version of index that displays only templates that belong to the user's org # GET /org_admin/templates/organisational # ----------------------------------------------------- - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity def organisational authorize Template templates = Template.latest_version_per_org(current_user.org.id) @@ -45,12 +44,12 @@ def organisational @orgs = current_user.can_super_admin? ? Org.all : nil @title = if current_user.can_super_admin? - _("%{org_name} Templates") % { org_name: current_user.org.name } + format(_('%{org_name} Templates'), org_name: current_user.org.name) else - _("Own Templates") + _('Own Templates') end @templates = templates.page(1) - @query_params = { sort_field: "templates.title", sort_direction: "asc" } + @query_params = { sort_field: 'templates.title', sort_direction: 'asc' } @all_count = templates.length @published_count = published.present? ? published : 0 @unpublished_count = if published.present? @@ -60,12 +59,13 @@ def organisational end render :index end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity # A version of index that displays only templates that are customizable # GET /org_admin/templates/customisable # ----------------------------------------------------- - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def customisable authorize Template customizations = Template.latest_customized_version_per_org(current_user.org.id) @@ -81,10 +81,10 @@ def customisable published = customizations.select { |t| t.published? || t.draft? }.length @orgs = current_user.can_super_admin? ? Org.all : [] - @title = _("Customisable Templates") + @title = _('Customizable Templates') @templates = funder_templates @customizations = customizations - @query_params = { sort_field: "templates.title", sort_direction: "asc" } + @query_params = { sort_field: 'templates.title', sort_direction: 'asc' } @all_count = funder_templates.length @published_count = published.present? ? published : 0 @unpublished_count = if published.present? @@ -96,7 +96,8 @@ def customisable render :index end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # GET /org_admin/templates/[:id] def show @@ -105,17 +106,17 @@ def show # Load the info needed for the overview section if the authorization check passes! phases = template.phases .includes(sections: { questions: :question_options }) - .order("phases.number", "sections.number", "questions.number", - "question_options.number") - .select("phases.title", "phases.description", "sections.title", - "questions.text", "question_options.text") + .order('phases.number', 'sections.number', 'questions.number', + 'question_options.number') + .select('phases.title', 'phases.description', 'sections.title', + 'questions.text', 'question_options.text') unless template.latest? # rubocop:disable Layout/LineLength - flash[:notice] = _("You are viewing a historical version of this template. You will not be able to make changes.") + flash[:notice] = _('You are viewing a historical version of this template. You will not be able to make changes.') # rubocop:enable Layout/LineLength end - render "container", locals: { - partial_path: "show", + render 'container', locals: { + partial_path: 'show', template: template, phases: phases, referrer: get_referrer(template, request.referrer) @@ -123,31 +124,33 @@ def show end # GET /org_admin/templates/:id/edit + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def edit template = Template.includes(:org, :phases).find(params[:id]) authorize template # Load the info needed for the overview section if the authorization check passes! phases = template.phases.includes(sections: { questions: :question_options }) - .order("phases.number", - "sections.number", - "questions.number", - "question_options.number") - .select("phases.title", - "phases.description", - "sections.title", - "questions.text", - "question_options.text") - if !template.latest? - redirect_to org_admin_template_path(id: template.id) - else - render "container", locals: { - partial_path: "edit", + .order('phases.number', + 'sections.number', + 'questions.number', + 'question_options.number') + .select('phases.title', + 'phases.description', + 'sections.title', + 'questions.text', + 'question_options.text') + if template.latest? + render 'container', locals: { + partial_path: 'edit', template: template, phases: phases, referrer: get_referrer(template, request.referrer) } + else + redirect_to org_admin_template_path(id: template.id) end end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength # GET /org_admin/templates/new def new @@ -167,16 +170,16 @@ def create @template = Template.new(args) @template.org_id = current_user.org.id @template.locale = current_org.language.abbreviation - @template.links = if params["template-links"].present? - ActiveSupport::JSON.decode(params["template-links"]) + @template.links = if params['template-links'].present? + ActiveSupport::JSON.decode(params['template-links']) else - { "funder": [], "sample_plan": [] } + { funder: [], sample_plan: [] } end if @template.save redirect_to edit_org_admin_template_path(@template), - notice: success_message(@template, _("created")) + notice: success_message(@template, _('created')) else - flash[:alert] = flash[:alert] = failure_message(@template, _("create")) + flash[:alert] = flash[:alert] = failure_message(@template, _('create')) render :new end end @@ -194,25 +197,23 @@ def update args[:visibility] = parse_visibility(args, current_user.org) template.assign_attributes(args) - if params["template-links"].present? - template.links = ActiveSupport::JSON.decode(params["template-links"]) - end + template.links = ActiveSupport::JSON.decode(params['template-links']) if params['template-links'].present? if template.save render(json: { status: 200, - msg: success_message(template, _("saved")) + msg: success_message(template, _('saved')) }) else render(json: { status: :bad_request, - msg: failure_message(template, _("save")) + msg: failure_message(template, _('save')) }) end rescue ActiveSupport::JSON.parse_error render(json: { status: :bad_request, - msg: _("Error parsing links for a %{template}") % - { template: template_type(template) } + msg: format(_('Error parsing links for a %{template}'), + template: template_type(template)) }) nil rescue StandardError => e @@ -225,7 +226,7 @@ def update # rubocop:enable Metrics/AbcSize, Metrics/MethodLength # DELETE /org_admin/templates/:id - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity def destroy template = Template.find(params[:id]) authorize template @@ -233,17 +234,13 @@ def destroy if versions.reject { |t| t.plans.empty? }.empty? versions.each do |version| if version.destroy! - flash[:notice] = success_message(template, _("removed")) + flash[:notice] = success_message(template, _('removed')) else - flash[:alert] = failure_message(template, _("remove")) + flash[:alert] = failure_message(template, _('remove')) end end else - # rubocop:disable Metrics/LineLength - flash[:alert] = _("You cannot delete a %{template_type} that has been used to create plans.") % { - template_type: template_type(template) - } - # rubocop:enable Metrics/LineLength + flash[:alert] = _("You cannot delete a #{template_type(template)} that has been used to create plans.") end if request.referrer.present? redirect_to request.referrer @@ -251,7 +248,7 @@ def destroy redirect_to org_admin_templates_path end end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity # GET /org_admin/templates/:id/history def history @@ -263,56 +260,48 @@ def history else organisational_org_admin_templates_path end - render "history", locals: { + render 'history', locals: { templates: templates, - query_params: { sort_field: "templates.version", sort_direction: "desc" }, + query_params: { sort_field: 'templates.version', sort_direction: 'desc' }, referrer: local_referrer, current: templates.maximum(:version) } end # PATCH /org_admin/templates/:id/publish (AJAX) + # rubocop:disable Metrics/AbcSize def publish template = Template.find(params[:id]) authorize template - # rubocop:disable Layout/LineLength publishable, errors = template.publishability if publishable if template.publish! - flash[:notice] = _("Your %{template_type} has been published and is now available to users.") % { - template_type: template_type(template) - } + flash[:notice] = _("Your #{template_type(template)} has been published and is now available to users.") else - flash[:alert] = _("Unable to publish your %{template_type}.") % { - template_type: template_type(template) - } + flash[:alert] = _("Unable to publish your #{template_type(template)}.") end else flash[:alert] = errors end - # rubocop:enable Layout/LineLength redirect_to request.referrer.present? ? request.referrer : org_admin_templates_path end + # rubocop:enable Metrics/AbcSize # PATCH /org_admin/templates/:id/unpublish (AJAX) + # rubocop:disable Metrics/AbcSize def unpublish template = Template.find(params[:id]) authorize template - versions = Template.where(family_id: template.family_id) - versions.each do |version| - unless version.update_attributes!(published: false) - flash[:alert] = _("Unable to unpublish your %{template_type}.") % { - template_type: template_type(template) - } - end - end - unless flash[:alert].present? - flash[:notice] = _("Successfully unpublished your %{template_type}") % { - template_type: template_type(template) - } + Template.transaction do + # expected: template is latest + template.generate_version! if template.published? && template.plans.any? + Template.where(family_id: template.family_id) + .update_all(published: false) end + flash[:notice] = _("Successfully unpublished your #{template_type(template)}") unless flash[:alert].present? redirect_to request.referrer.present? ? request.referrer : org_admin_templates_path end + # rubocop:enable Metrics/AbcSize # GET template_export/:id # ----------------------------------------------------- @@ -338,36 +327,32 @@ def template_export @formatting = Settings::Template::DEFAULT_SETTINGS[:formatting] begin - # rubocop:disable Layout/LineLength - file_name = @template.title.gsub(/[^a-zA-Z\d\s]/, "").gsub(/ /, "_") + "_v" + @template.version.to_s - # rubocop:enable Layout/LineLength + safe_title = @template.title.gsub(/[^a-zA-Z\d\s]/, '').gsub(/ /, '_') + file_name = "#{safe_title}_v#{@template.version}" respond_to do |format| format.docx do - render docx: "template_exports/template_export", filename: "#{file_name}.docx" + render docx: 'template_exports/template_export', filename: "#{file_name}.docx" end format.pdf do # rubocop:disable Layout/LineLength render pdf: file_name, - template: "template_exports/template_export", - margin: @formatting[:margin], - footer: { - center: _("Template created using the %{application_name} service. Last modified %{date}") % { - application_name: _(Rails.configuration.branding[:application][:name]), - date: l(@template.updated_at.to_date, formats: :short) - }, - font_size: 8, - spacing: (@formatting[:margin][:bottom] / 2) - 4, - right: _("[page] of [topage]"), - encoding: "utf8" - } - # rubocop:enable Metrics/LineLength + template: 'template_exports/template_export', + margin: @formatting[:margin], + footer: { + center: format(_('Template created using the %{application_name} service. Last modified %{date}'), application_name: ApplicationService.application_name, date: l(@template.updated_at.to_date, formats: :short)), + font_size: 8, + spacing: (@formatting[:margin][:bottom] / 2) - 4, + right: '[page] of [topage]', + encoding: 'utf8' + } + # rubocop:enable Layout/LineLength end end rescue ActiveRecord::RecordInvalid # What scenario is this triggered in? it's common to our export pages redirect_to public_templates_path, - alert: _("Unable to download the DMP Template at this time.") + alert: _('Unable to download the DMP Template at this time.') end end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength @@ -399,9 +384,9 @@ def parse_visibility(args, org) # If nil and the org is not a funder, we default to organisational # If present, we parse to retrieve the value if args[:visibility].nil? - org.funder? ? "publicly_visible" : "organisationally_visible" + org.funder? ? 'publicly_visible' : 'organisationally_visible' else - args.fetch(:visibility, "0") == "1" ? "organisationally_visible" : "publicly_visible" + args.fetch(:visibility, '0') == '1' ? 'organisationally_visible' : 'publicly_visible' end end @@ -421,8 +406,5 @@ def get_referrer(template, referrer) request.referrer end end - end - # rubocop:enable Metrics/ClassLength - end diff --git a/app/controllers/org_admin/users_controller.rb b/app/controllers/org_admin/users_controller.rb index 8b096f55f8..6138ddb18b 100644 --- a/app/controllers/org_admin/users_controller.rb +++ b/app/controllers/org_admin/users_controller.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true module OrgAdmin - + # Controller that handles admin operations on users class UsersController < ApplicationController - after_action :verify_authorized def edit @@ -11,7 +10,7 @@ def edit authorize @user @departments = @user.org.departments.order(:name) @plans = Plan.active(@user).page(1) - render "org_admin/users/edit", + render 'org_admin/users/edit', locals: { user: @user, departments: @departments, plans: @plans, @@ -21,24 +20,26 @@ def edit default_org: @user.org } end + # rubocop:disable Metrics/AbcSize def update @user = User.find(params[:id]) authorize @user @departments = @user.org.departments.order(:name) @plans = Plan.active(@user).page(1) if @user.update_attributes(user_params) - flash.now[:notice] = success_message(@user, _("updated")) + flash.now[:notice] = success_message(@user, _('updated')) else - flash.now[:alert] = failure_message(@user, _("update")) + flash.now[:alert] = failure_message(@user, _('update')) end render :edit end + # rubocop:enable Metrics/AbcSize def user_plans @user = User.find(params[:id]) authorize @user @plans = Plan.active(@user).page(1) - render "org_admin/users/plans" + render 'org_admin/users/plans' end private @@ -46,7 +47,5 @@ def user_plans def user_params params.require(:user).permit(:department_id) end - end - end diff --git a/app/controllers/orgs_controller.rb b/app/controllers/orgs_controller.rb index f302afd450..ed0e3f78df 100644 --- a/app/controllers/orgs_controller.rb +++ b/app/controllers/orgs_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Controller for Org pages for Admins class OrgsController < ApplicationController - include OrgSelectable after_action :verify_authorized, except: %w[ @@ -19,30 +19,36 @@ class OrgsController < ApplicationController def admin_edit org = Org.find(params[:id]) authorize org - languages = Language.all.order("name") - org.links = { "org": [] } unless org.links.present? - render "admin_edit", locals: { org: org, languages: languages, method: "PUT", + languages = Language.all.order('name') + org.links = { org: [] } unless org.links.present? + render 'admin_edit', locals: { org: org, languages: languages, method: 'PUT', url: admin_update_org_path(org) } end # PUT /org/admin/:id/admin_update # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def admin_update attrs = org_params @org = Org.find(params[:id]) authorize @org - @org.logo = attrs[:logo] if attrs[:logo] - tab = (attrs[:feedback_enabled].present? ? "feedback" : "profile") + + # If a new logo was supplied then use it, otherwise retain the existing one + attrs[:logo] = attrs[:logo].present? ? attrs[:logo] : @org.logo + # Remove the logo if the user checked the box + attrs[:logo] = nil if attrs[:remove_logo] == '1' + + tab = (attrs[:feedback_enabled].present? ? 'feedback' : 'profile') @org.links = ActiveSupport::JSON.decode(params[:org_links]) if params[:org_links].present? # Only allow super admins to change the org types and shib info if current_user.can_super_admin? identifiers = [] - attrs[:managed] = attrs[:managed] == "1" + attrs[:managed] = attrs[:managed] == '1' # Handle Shibboleth identifier if that is enabled if Rails.configuration.x.shibboleth.use_filtered_discovery_service - shib = IdentifierScheme.by_name("shibboleth").first + shib = IdentifierScheme.by_name('shibboleth').first if shib.present? && attrs[:identifiers_attributes].present? key = attrs[:identifiers_attributes].keys.first @@ -82,19 +88,19 @@ def admin_update end @org.save end - - redirect_to "#{admin_edit_org_path(@org)}\##{tab}", - notice: success_message(@org, _("saved")) + redirect_to "#{admin_edit_org_path(@org)}##{tab}", + notice: success_message(@org, _('saved')) else - failure = failure_message(@org, _("save")) if failure.blank? - redirect_to "#{admin_edit_org_path(@org)}\##{tab}", alert: failure + failure = failure_message(@org, _('save')) if failure.blank? + redirect_to "#{admin_edit_org_path(@org)}##{tab}", alert: failure end end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # This action is used by installations that have the following config enabled: # Rails.configuration.x.shibboleth.use_filtered_discovery_service + # rubocop:disable Metrics/AbcSize def shibboleth_ds unless current_user.nil? redirect_to root_path @@ -103,7 +109,7 @@ def shibboleth_ds @user = User.new # Display the custom Shibboleth discovery service page. - @orgs = Identifier.by_scheme_name("shibboleth", "Org") + @orgs = Identifier.by_scheme_name('shibboleth', 'Org') .sort { |a, b| a.identifiable.name <=> b.identifiable.name } .map(&:identifiable) @@ -111,7 +117,7 @@ def shibboleth_ds # if the ``@orgs` array has items ... it renders the shibboleth_ds view # rubocop:disable Style/GuardClause, Style/RedundantReturn if @orgs.empty? - flash.now[:alert] = _("No organisations are currently registered.") + flash.now[:alert] = _('No organisations are currently registered.') redirect_to user_shibboleth_omniauth_authorize_path return end @@ -120,41 +126,41 @@ def shibboleth_ds # This action is used to redirect a user to the Shibboleth IdP # POST /orgs/shibboleth_ds - # rubocop:disable Metrics/AbcSize def shibboleth_ds_passthru - if !shib_params[:org_id].blank? - session["org_id"] = shib_params[:org_id] + if shib_params[:org_id].blank? + redirect_to shibboleth_ds_path, notice: _('Please choose an organisation') + else + session['org_id'] = shib_params[:org_id] org = Org.where(id: shib_params[:org_id]) - shib_entity = Identifier.by_scheme_name("shibboleth", "Org") + shib_entity = Identifier.by_scheme_name('shibboleth', 'Org') .where(identifiable: org) - if !shib_entity.empty? + if shib_entity.empty? + failure = _('Your organisation does not seem to be properly configured.') + redirect_to shibboleth_ds_path, alert: failure + else # initiate shibboleth login sequence entity_param = "entityID=#{shib_entity.first.value}" redirect_to "#{shib_login_url}?#{shib_callback_url}&#{entity_param}" - else - failure = _("Your organisation does not seem to be properly configured.") - redirect_to shibboleth_ds_path, alert: failure end - else - redirect_to shibboleth_ds_path, notice: _("Please choose an organisation") end end # rubocop:enable Metrics/AbcSize # POST /orgs (via AJAX from Org Typeaheads ... see below for specific pages) - # rubocop:disable Metrics/MethodLength, Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def search args = search_params # If the search term is greater than 2 characters - if args.present? && args.fetch(:name, "").length > 2 - type = params.fetch(:type, "local") + if args.present? && args.fetch(:name, '').length > 2 + type = params.fetch(:type, 'local') # If we are including external API results orgs = case type - when "combined" + when 'combined' # This type will search both ROR and the local DB giving the local # DB results preference. It is triggered from the following pages: # Create Account @@ -167,7 +173,7 @@ def search OrgSelection::SearchService.search_combined( search_term: args[:name] ) - when "external" + when 'external' # This type will ONLY check ROR for the specified search term. It # is triggered from the following page: # SuperAdmin - New Org @@ -194,7 +200,7 @@ def search # If we need to restrict the results to funding orgs then # only return the ones with a valid fundref - if orgs.present? && params.fetch(:funder_only, "false") == "true" + if orgs.present? && params.fetch(:funder_only, 'false') == 'true' orgs = orgs.select do |org| org[:fundref].present? && !org[:fundref].blank? end @@ -206,7 +212,8 @@ def search render json: [] end end - # rubocop:enable Metrics/MethodLength, Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity private @@ -216,12 +223,13 @@ def org_params :remove_logo, :managed, :feedback_enabled, :org_links, :funder, :institution, :organisation, :feedback_msg, :org_id, :org_name, :org_crosswalk, + :helpdesk_email, identifiers_attributes: %i[identifier_scheme_id value], tracker_attributes: %i[code id]) end def shib_params - params.permit("org_id") + params.permit('org_id') end def search_params @@ -240,6 +248,7 @@ def shib_callback_url # Destroy the identifier if it exists and was blanked out, replace the # identifier if it was updated, create the identifier if its new, or # ignore it + # rubocop:disable Metrics/AbcSize def process_identifier_change(org:, identifier:) return org unless identifier.is_a?(Identifier) @@ -257,5 +266,5 @@ def process_identifier_change(org:, identifier:) org end - + # rubocop:enable Metrics/AbcSize end diff --git a/app/controllers/paginable/contributors_controller.rb b/app/controllers/paginable/contributors_controller.rb index b0a153f6f4..3e50ef5b65 100644 --- a/app/controllers/paginable/contributors_controller.rb +++ b/app/controllers/paginable/contributors_controller.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true module Paginable - + # Controller for paginating/sorting/searching the contributors table class ContributorsController < ApplicationController - after_action :verify_authorized respond_to :html @@ -13,15 +12,13 @@ class ContributorsController < ApplicationController # GET /paginable/plans/:plan_id/contributors/index/:page def index @plan = Plan.find_by(id: params[:plan_id]) - authorize @plan + authorize @plan, :show? paginable_renderise( - partial: "index", + partial: 'index', scope: Contributor.where(plan_id: @plan.id), - query_params: { sort_field: "contributors.name", sort_direction: :asc }, + query_params: { sort_field: 'contributors.name', sort_direction: :asc }, format: :json ) end - end - end diff --git a/app/controllers/paginable/departments_controller.rb b/app/controllers/paginable/departments_controller.rb index 0fd997d636..77affdcb12 100644 --- a/app/controllers/paginable/departments_controller.rb +++ b/app/controllers/paginable/departments_controller.rb @@ -1,31 +1,32 @@ # frozen_string_literal: true -class Paginable::DepartmentsController < ApplicationController +module Paginable + # Controller for paginating/sorting/searching the departments table + class DepartmentsController < ApplicationController + after_action :verify_authorized + respond_to :html - after_action :verify_authorized - respond_to :html + include Paginable - include Paginable - - # /paginable/departments/index/:page - def index - authorize Department - paginable_renderise( - partial: "index", - scope: departments, - query_params: { sort_field: "departments.name", sort_direction: :asc }, - format: :json - ) - end + # /paginable/departments/index/:page + def index + authorize Department + paginable_renderise( + partial: 'index', + scope: departments, + query_params: { sort_field: 'departments.name', sort_direction: :asc }, + format: :json + ) + end - private + private - def departments - if current_user.can_super_admin? - Department.by_org(Org.find(params[:id])) - else - Department.by_org(Org.find(current_user.org_id)) + def departments + if current_user.can_super_admin? + Department.by_org(Org.find(params[:id])) + else + Department.by_org(Org.find(current_user.org_id)) + end end end - end diff --git a/app/controllers/paginable/guidance_groups_controller.rb b/app/controllers/paginable/guidance_groups_controller.rb index 761d6d9af7..d001ecc68a 100644 --- a/app/controllers/paginable/guidance_groups_controller.rb +++ b/app/controllers/paginable/guidance_groups_controller.rb @@ -1,18 +1,19 @@ # frozen_string_literal: true -class Paginable::GuidanceGroupsController < ApplicationController +module Paginable + # Controller for paginating/sorting/searching the guidance groups table + class GuidanceGroupsController < ApplicationController + include Paginable - include Paginable - - # /paginable/guidance_groups/index/:page - def index - authorize(Guidance) - paginable_renderise( - partial: "index", - scope: GuidanceGroup.by_org(current_user.org), - query_params: { sort_field: "guidance_groups.name", sort_direction: :asc }, - format: :json - ) + # /paginable/guidance_groups/index/:page + def index + authorize(Guidance) + paginable_renderise( + partial: 'index', + scope: GuidanceGroup.by_org(current_user.org), + query_params: { sort_field: 'guidance_groups.name', sort_direction: :asc }, + format: :json + ) + end end - end diff --git a/app/controllers/paginable/guidances_controller.rb b/app/controllers/paginable/guidances_controller.rb index 6d4d9bfb57..d97e00826a 100644 --- a/app/controllers/paginable/guidances_controller.rb +++ b/app/controllers/paginable/guidances_controller.rb @@ -1,19 +1,20 @@ # frozen_string_literal: true -class Paginable::GuidancesController < ApplicationController +module Paginable + # Controller for paginating/sorting/searching the guidance table + class GuidancesController < ApplicationController + include Paginable - include Paginable - - # /paginable/guidances/index/:page - def index - authorize(Guidance) - paginable_renderise( - partial: "index", - scope: Guidance.by_org(current_user.org) - .includes(:guidance_group, :themes), - query_params: { sort_field: "guidances.text", sort_direction: :asc }, - format: :json - ) + # /paginable/guidances/index/:page + def index + authorize(Guidance) + paginable_renderise( + partial: 'index', + scope: Guidance.by_org(current_user.org) + .includes(:guidance_group, :themes), + query_params: { sort_field: 'guidances.text', sort_direction: :asc }, + format: :json + ) + end end - end diff --git a/app/controllers/paginable/notifications_controller.rb b/app/controllers/paginable/notifications_controller.rb index 3aa7b088a8..5b64066edd 100644 --- a/app/controllers/paginable/notifications_controller.rb +++ b/app/controllers/paginable/notifications_controller.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true -class Paginable::NotificationsController < ApplicationController - - include Paginable - - # /paginable/notifications/index/:page - def index - authorize(Notification) - paginable_renderise(partial: "index", scope: Notification.all, format: :json) +module Paginable + # Controller for paginating/sorting/searching the notifications table + class NotificationsController < ApplicationController + include Paginable + + # /paginable/notifications/index/:page + def index + authorize(Notification) + paginable_renderise(partial: 'index', scope: Notification.all, format: :json) + end end - end diff --git a/app/controllers/paginable/orgs_controller.rb b/app/controllers/paginable/orgs_controller.rb index e35944a54b..252f352d54 100644 --- a/app/controllers/paginable/orgs_controller.rb +++ b/app/controllers/paginable/orgs_controller.rb @@ -1,18 +1,19 @@ # frozen_string_literal: true -class Paginable::OrgsController < ApplicationController +module Paginable + # Controller for paginating/sorting/searching the orgs table + class OrgsController < ApplicationController + include Paginable - include Paginable - - # /paginable/guidances/index/:page - def index - authorize(Org) - paginable_renderise( - partial: "index", - scope: Org.with_template_and_user_counts, - query_params: { sort_field: "orgs.name", sort_direction: :asc }, - format: :json - ) + # /paginable/guidances/index/:page + def index + authorize(Org) + paginable_renderise( + partial: 'index', + scope: Org.with_template_and_user_counts, + query_params: { sort_field: 'orgs.name', sort_direction: :asc }, + format: :json + ) + end end - end diff --git a/app/controllers/paginable/plans_controller.rb b/app/controllers/paginable/plans_controller.rb index e29d26bdf9..b4ddbae87f 100644 --- a/app/controllers/paginable/plans_controller.rb +++ b/app/controllers/paginable/plans_controller.rb @@ -1,77 +1,75 @@ # frozen_string_literal: true -class Paginable::PlansController < ApplicationController +module Paginable + # Controller for paginating/sorting/searching the plans tables + class PlansController < ApplicationController + include Paginable - include Paginable + # /paginable/plans/privately_visible/:page + def privately_visible + authorize Plan - # /paginable/plans/privately_visible/:page - def privately_visible - unless Paginable::PlanPolicy.new(current_user).privately_visible? - raise Pundit::NotAuthorizedError + paginable_renderise( + partial: 'privately_visible', + scope: Plan.active(current_user), + query_params: { sort_field: 'plans.updated_at', sort_direction: :desc }, + format: :json + ) end - paginable_renderise( - partial: "privately_visible", - scope: Plan.active(current_user), - query_params: { sort_field: "plans.updated_at", sort_direction: :desc }, - format: :json - ) - end + # GET /paginable/plans/organisationally_or_publicly_visible/:page + def organisationally_or_publicly_visible + authorize Plan - # GET /paginable/plans/organisationally_or_publicly_visible/:page - def organisationally_or_publicly_visible - unless Paginable::PlanPolicy.new(current_user).organisationally_or_publicly_visible? - raise Pundit::NotAuthorizedError + paginable_renderise( + partial: 'organisationally_or_publicly_visible', + scope: Plan.organisationally_or_publicly_visible(current_user), + query_params: { sort_field: 'plans.updated_at', sort_direction: :desc }, + format: :json + ) end - paginable_renderise( - partial: "organisationally_or_publicly_visible", - scope: Plan.organisationally_or_publicly_visible(current_user), - query_params: { sort_field: "plans.updated_at", sort_direction: :desc }, - format: :json - ) - end + # GET /paginable/plans/publicly_visible/:page + def publicly_visible + # We want the pagination/sort/search to be retained in the URL so redirect instead + # of processing this as a JSON + paginable_params = params.permit(:page, :search, :sort_field, :sort_direction) + redirect_to public_plans_path(paginable_params.to_h) + end - # GET /paginable/plans/publicly_visible/:page - def publicly_visible - # We want the pagination/sort/search to be retained in the URL so redirect instead - # of processing this as a JSON - paginable_params = params.permit(:page, :search, :sort_field, :sort_direction) - redirect_to public_plans_path(paginable_params.to_h) - end + # GET /paginable/plans/org_admin/:page + # rubocop:disable Metrics/AbcSize + def org_admin + raise Pundit::NotAuthorizedError unless current_user.present? && current_user.can_org_admin? - # GET /paginable/plans/org_admin/:page - def org_admin - raise Pundit::NotAuthorizedError unless current_user.present? && current_user.can_org_admin? + # check if current user if super_admin + @super_admin = current_user.can_super_admin? + @clicked_through = params[:click_through].present? + plans = @super_admin ? Plan.all : current_user.org.org_admin_plans + plans = plans.joins(:template, roles: [user: :org]).where(Role.creator_condition) - # check if current user if super_admin - @super_admin = current_user.can_super_admin? - @clicked_through = params[:click_through].present? - plans = @super_admin ? Plan.all : current_user.org.org_admin_plans - plans = plans.joins(:template, roles: [user: :org]).where(Role.creator_condition) + paginable_renderise( + partial: 'org_admin', + scope: plans, + view_all: !current_user.can_super_admin?, + query_params: { sort_field: 'plans.updated_at', sort_direction: :desc }, + format: :json + ) + end + # rubocop:enable Metrics/AbcSize - paginable_renderise( - partial: "org_admin", - scope: plans, - view_all: !current_user.can_super_admin?, - query_params: { sort_field: "plans.updated_at", sort_direction: :desc }, - format: :json - ) - end + # GET /paginable/users/:id/plans + def index + @user = User.find(params[:id]) + authorize @user + raise Pundit::NotAuthorizedError unless current_user.present? && current_user.can_org_admin? && @user.present? - # GET /paginable/plans/org_admin/:page - def org_admin_other_user - @user = User.find(params[:id]) - authorize @user - unless current_user.present? && current_user.can_org_admin? && @user.present? - raise Pundit::NotAuthorizedError + paginable_renderise( + partial: 'index', + scope: Plan.active(@user), + query_params: { sort_field: 'plans.updated_at', sort_direction: :desc }, + format: :json + ) end - - paginable_renderise( - partial: "org_admin_other_user", - scope: Plan.active(@user), - query_params: { sort_field: "plans.updated_at", sort_direction: :desc } - ) end - end diff --git a/app/controllers/paginable/templates_controller.rb b/app/controllers/paginable/templates_controller.rb index 680e7eb8ca..5ea6b552e8 100644 --- a/app/controllers/paginable/templates_controller.rb +++ b/app/controllers/paginable/templates_controller.rb @@ -1,109 +1,113 @@ # frozen_string_literal: true -class Paginable::TemplatesController < ApplicationController +module Paginable + # Controller for paginating/sorting/searching the templates tables + class TemplatesController < ApplicationController + include CustomizableTemplateLinkHelper + include Paginable - include CustomizableTemplateLinkHelper - include Paginable + # TODO: Clean up this code for Rubocop - # TODO: Clean up this code for Rubocop - # rubocop:disable Layout/LineLength - - # GET /paginable/templates/:page (AJAX) - # ----------------------------------------------------- - def index - authorize Template - templates = Template.latest_version.where(customization_of: nil) - case params[:f] - when "published" - template_ids = templates.select { |t| t.published? || t.draft? }.collect(&:family_id) - templates = Template.latest_version(template_ids).where(customization_of: nil) - when "unpublished" - template_ids = templates.select { |t| !t.published? && !t.draft? }.collect(&:family_id) - templates = Template.latest_version(template_ids).where(customization_of: nil) + # GET /paginable/templates/:page (AJAX) + # ----------------------------------------------------- + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity + def index + authorize Template + templates = Template.latest_version.where(customization_of: nil) + case params[:f] + when 'published' + template_ids = templates.select { |t| t.published? || t.draft? }.collect(&:family_id) + templates = Template.latest_version(template_ids).where(customization_of: nil) + when 'unpublished' + template_ids = templates.select { |t| !t.published? && !t.draft? }.collect(&:family_id) + templates = Template.latest_version(template_ids).where(customization_of: nil) + end + paginable_renderise( + partial: 'index', + scope: templates.includes(:org), + query_params: { sort_field: 'templates.title', sort_direction: :asc }, + locals: { action: 'index' }, + format: :json + ) end - paginable_renderise( - partial: "index", - scope: templates.includes(:org), - query_params: { sort_field: "templates.title", sort_direction: :asc }, - locals: { action: "index" }, - format: :json - ) - end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity - # GET /paginable/templates/organisational/:page (AJAX) - # ----------------------------------------------------- - def organisational - authorize Template - templates = Template.latest_version_per_org(current_user.org.id) - .where(customization_of: nil, org_id: current_user.org.id) - case params[:f] - when "published" - template_ids = templates.select { |t| t.published? || t.draft? }.collect(&:family_id) - templates = Template.latest_version(template_ids) - when "unpublished" - template_ids = templates.select { |t| !t.published? && !t.draft? }.collect(&:family_id) - templates = Template.latest_version(template_ids) + # GET /paginable/templates/organisational/:page (AJAX) + # ----------------------------------------------------- + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity + def organisational + authorize Template + templates = Template.latest_version_per_org(current_user.org.id) + .where(customization_of: nil, org_id: current_user.org.id) + case params[:f] + when 'published' + template_ids = templates.select { |t| t.published? || t.draft? }.collect(&:family_id) + templates = Template.latest_version(template_ids) + when 'unpublished' + template_ids = templates.select { |t| !t.published? && !t.draft? }.collect(&:family_id) + templates = Template.latest_version(template_ids) + end + paginable_renderise( + partial: 'organisational', + scope: templates, + query_params: { sort_field: 'templates.title', sort_direction: :asc }, + locals: { action: 'organisational' }, + format: :json + ) end - paginable_renderise( - partial: "organisational", - scope: templates, - query_params: { sort_field: "templates.title", sort_direction: :asc }, - locals: { action: "organisational" }, - format: :json - ) - end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity - # GET /paginable/templates/customisable/:page (AJAX) - # ----------------------------------------------------- - # rubocop:disable Metrics/AbcSize - def customisable - authorize Template - customizations = Template.latest_customized_version_per_org(current_user.org.id) - templates = Template.latest_customizable - case params[:f] - when "published" - customization_ids = customizations.select { |t| t.published? || t.draft? }.collect(&:customization_of) - templates = Template.latest_customizable.where(family_id: customization_ids) - when "unpublished" - customization_ids = customizations.select { |t| !t.published? && !t.draft? }.collect(&:customization_of) - templates = Template.latest_customizable.where(family_id: customization_ids) - when "not-customised" - templates = Template.latest_customizable.where.not(family_id: customizations.collect(&:customization_of)) + # GET /paginable/templates/customisable/:page (AJAX) + # ----------------------------------------------------- + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def customisable + authorize Template + customizations = Template.latest_customized_version_per_org(current_user.org.id) + templates = Template.latest_customizable + case params[:f] + when 'published' + customization_ids = customizations.select { |t| t.published? || t.draft? }.collect(&:customization_of) + templates = Template.latest_customizable.where(family_id: customization_ids) + when 'unpublished' + customization_ids = customizations.select { |t| !t.published? && !t.draft? }.collect(&:customization_of) + templates = Template.latest_customizable.where(family_id: customization_ids) + when 'not-customised' + templates = Template.latest_customizable.where.not(family_id: customizations.collect(&:customization_of)) + end + paginable_renderise( + partial: 'customisable', + scope: templates.joins(:org).includes(:org), + query_params: { sort_field: 'templates.title', sort_direction: :asc }, + locals: { action: 'customisable', customizations: customizations }, + format: :json + ) end - paginable_renderise( - partial: "customisable", - scope: templates.joins(:org).includes(:org), - query_params: { sort_field: "templates.title", sort_direction: :asc }, - locals: { action: "customisable", customizations: customizations }, - format: :json - ) - end - # rubocop:enable Metrics/AbcSize - - # rubocop:enable Layout/LineLength + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity - # GET /paginable/templates/publicly_visible/:page (AJAX) - # ----------------------------------------------------- - def publicly_visible - # We want the pagination/sort/search to be retained in the URL so redirect instead - # of processing this as a JSON - paginable_params = params.permit(:page, :search, :sort_field, :sort_direction) - redirect_to public_templates_path(paginable_params.to_h) - end + # GET /paginable/templates/publicly_visible/:page (AJAX) + # ----------------------------------------------------- + def publicly_visible + # We want the pagination/sort/search to be retained in the URL so redirect instead + # of processing this as a JSON + paginable_params = params.permit(:page, :search, :sort_field, :sort_direction) + redirect_to public_templates_path(paginable_params.to_h) + end - # GET /paginable/templates/:id/history/:page (AJAX) - # ----------------------------------------------------- - def history - @template = Template.find(params[:id]) - authorize @template - @templates = Template.where(family_id: @template.family_id) - @current = Template.current(@template.family_id) - paginable_renderise( - partial: "history", - scope: @templates, - query_params: { sort_field: "templates.title", sort_direction: :asc }, - locals: { current: @templates.maximum(:version) } - ) + # GET /paginable/templates/:id/history/:page (AJAX) + # ----------------------------------------------------- + def history + @template = Template.find(params[:id]) + authorize @template + @templates = Template.where(family_id: @template.family_id) + @current = Template.current(@template.family_id) + paginable_renderise( + partial: 'history', + scope: @templates, + query_params: { sort_field: 'templates.title', sort_direction: :asc }, + locals: { current: @templates.maximum(:version) } + ) + end end - end diff --git a/app/controllers/paginable/themes_controller.rb b/app/controllers/paginable/themes_controller.rb index 06cfab2b6c..19dbe3356e 100644 --- a/app/controllers/paginable/themes_controller.rb +++ b/app/controllers/paginable/themes_controller.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true -class Paginable::ThemesController < ApplicationController - - include Paginable - - # /paginable/themes/index/:page - def index - authorize(Theme) - paginable_renderise(partial: "index", scope: Theme.all, format: :json) +module Paginable + # Controller for paginating/sorting/searching the themes table + class ThemesController < ApplicationController + include Paginable + + # /paginable/themes/index/:page + def index + authorize(Theme) + paginable_renderise(partial: 'index', scope: Theme.all, format: :json) + end end - end diff --git a/app/controllers/paginable/users_controller.rb b/app/controllers/paginable/users_controller.rb index 9c9dfee77c..3da69b12cc 100644 --- a/app/controllers/paginable/users_controller.rb +++ b/app/controllers/paginable/users_controller.rb @@ -1,32 +1,35 @@ # frozen_string_literal: true -class Paginable::UsersController < ApplicationController - - include Paginable - - # /paginable/users/index/:page - def index - authorize User - @clicked_through = params[:click_through].present? - - # variable containing the check box value - @filter_admin = params[:filter_admin] == "1" - - scope = if current_user.can_super_admin? - User.includes(:roles) - else - current_user.org.users.includes(:roles) - end - - scope = scope.joins(:perms).distinct if @filter_admin - - paginable_renderise( - partial: "index", - scope: scope, - query_params: { sort_field: "users.surname", sort_direction: :asc }, - format: :json, - view_all: !current_user.can_super_admin? - ) +module Paginable + # Controller for paginating/sorting/searching the users table + class UsersController < ApplicationController + include Paginable + + # /paginable/users/index/:page + # rubocop:disable Metrics/AbcSize + def index + authorize User + @clicked_through = params[:click_through].present? + + # variable containing the check box value + @filter_admin = params[:filter_admin] == '1' + + scope = if current_user.can_super_admin? + User.includes(:roles) + else + current_user.org.users.includes(:roles) + end + + scope = scope.joins(:perms).distinct if @filter_admin + + paginable_renderise( + partial: 'index', + scope: scope, + query_params: { sort_field: 'users.surname', sort_direction: :asc }, + format: :json, + view_all: !current_user.can_super_admin? + ) + end + # rubocop:enable Metrics/AbcSize end - end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 616b438722..c1c4c715cc 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Controller for password resets that is built on the Devise gem class PasswordsController < Devise::PasswordsController - protected def after_resetting_password_path_for(_resource) @@ -18,5 +18,4 @@ def after_resetting_password_path_for(_resource) def after_sending_reset_password_instructions_path_for(_resource_name) root_path end - end diff --git a/app/controllers/plan_exports_controller.rb b/app/controllers/plan_exports_controller.rb index 7ce43e9c93..aa9e18d37a 100644 --- a/app/controllers/plan_exports_controller.rb +++ b/app/controllers/plan_exports_controller.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true +# Controller for the Plan Download page class PlanExportsController < ApplicationController - after_action :verify_authorized include ConditionsHelper # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity def show @plan = Plan.includes(:answers, { template: { phases: { sections: :questions } } }) .find(params[:plan_id]) @@ -17,6 +18,7 @@ def show @show_sections_questions = export_params[:question_headings].present? @show_unanswered = export_params[:unanswered_questions].present? @show_custom_sections = export_params[:custom_sections].present? + @show_research_outputs = export_params[:research_outputs].present? @public_plan = false elsif publicly_authorized? @@ -25,6 +27,7 @@ def show @show_sections_questions = true @show_unanswered = true @show_custom_sections = true + @show_research_outputs = @plan.research_outputs&.any? || false @public_plan = true else @@ -33,17 +36,16 @@ def show @hash = @plan.as_pdf(current_user, @show_coversheet) @formatting = export_params[:formatting] || @plan.settings(:export).formatting - if params.key?(:phase_id) && params[:phase_id].length.positive? # order phases by phase number asc @hash[:phases] = @hash[:phases].sort_by { |phase| phase[:number] } - if params[:phase_id] == "All" + if params[:phase_id] == 'All' @hash[:all_phases] = true else @selected_phase = @plan.phases.find(params[:phase_id]) end else - @selected_phase = @plan.phases.order("phases.updated_at DESC") + @selected_phase = @plan.phases.order('phases.updated_at DESC') .detect { |p| p.visibility_allowed?(@plan) } end @@ -64,6 +66,7 @@ def show end end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity private @@ -81,34 +84,33 @@ def show_csv end def show_text - send_data render_to_string(partial: "shared/export/plan_txt"), + send_data render_to_string(partial: 'shared/export/plan_txt'), filename: "#{file_name}.txt" end def show_docx # Using and optional locals_assign export_format render docx: "#{file_name}.docx", - content: render_to_string(partial: "shared/export/plan", - locals: { export_format: "docx" }) + content: render_to_string(partial: 'shared/export/plan', + locals: { export_format: 'docx' }) end def show_pdf render pdf: file_name, margin: @formatting[:margin], footer: { - center: _("Created using %{application_name}. Last modified %{date}") % { - application_name: ApplicationService.application_name, - date: l(@plan.updated_at.to_date, format: :readable) - }, + center: format(_('Created using %{application_name}. Last modified %{date}'), + application_name: ApplicationService.application_name, + date: l(@plan.updated_at.to_date, format: :readable)), font_size: 8, spacing: (Integer(@formatting[:margin][:bottom]) / 2) - 4, - right: _("[page] of [topage]"), - encoding: "UTF-8" + right: _('[page] of [topage]'), + encoding: 'UTF-8' } end def show_json - json = render_to_string(partial: "/api/v1/plans/show", locals: { plan: @plan }) + json = render_to_string(partial: '/api/v1/plans/show', locals: { plan: @plan }) render json: "{\"dmp\":#{json}}" end @@ -116,8 +118,8 @@ def file_name # Sanitize bad characters and replace spaces with underscores ret = @plan.title Zaru.sanitize! ret - ret = ret.strip.gsub(/\s+/, "_") - ret = ret.gsub(/"/, "") + ret = ret.strip.gsub(/\s+/, '_') + ret = ret.gsub(/"/, '') # limit the filename length to 100 chars. Windows systems have a MAX_PATH allowance # of 255 characters, so this should provide enough of the title to allow the user # to understand which DMP it is and still allow for the file to be saved to a deeply @@ -139,9 +141,9 @@ def privately_authorized? end def export_params - params.require(:export).permit(:form, :project_details, :question_headings, - :unanswered_questions, :custom_sections, - :formatting) + params.require(:export) + .permit(:form, :project_details, :question_headings, :unanswered_questions, + :custom_sections, :research_outputs, + formatting: [:font_face, :font_size, { margin: %i[top right bottom left] }]) end - end diff --git a/app/controllers/plans_controller.rb b/app/controllers/plans_controller.rb index a15397362b..96ce3ee3c7 100644 --- a/app/controllers/plans_controller.rb +++ b/app/controllers/plans_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true +# Controller for the Write plan and create plan pages # rubocop:disable Metrics/ClassLength class PlansController < ApplicationController - include ConditionalUserMailer include OrgSelectable @@ -10,22 +10,24 @@ class PlansController < ApplicationController helper SettingsTemplateHelper after_action :verify_authorized, except: [:overview] - before_action :setup_local_orgs, only: [:new, :show] + before_action :setup_local_orgs, only: %i[new show] + # GET /plans + # rubocop:disable Metrics/AbcSize def index authorize Plan @plans = Plan.includes(:roles, :org).active(current_user).page(1) - if current_user.org.is_other? - @organisationally_or_publicly_visible = [] - else - @organisationally_or_publicly_visible = - Plan.organisationally_or_publicly_visible(current_user).page(1) - end + @organisationally_or_publicly_visible = if current_user.org.is_other? + [] + else + Plan.organisationally_or_publicly_visible(current_user).page(1) + end # TODO: Is this still used? We cannot switch this to use the :plan_params # strong params because any calls that do not include `plan` in the # query string will fail @template = Template.find(params[:plan][:template_id]) if params[:plan].present? end + # rubocop:enable Metrics/AbcSize # GET /plans/new # rubocop:disable Metrics/AbcSize @@ -38,6 +40,11 @@ def new .includes(identifiers: :identifier_scheme) .joins(:templates) .where(templates: { published: true }).uniq.sort_by(&:name) + @orgs = (Org.includes(identifiers: :identifier_scheme).organisation + + Org.includes(identifiers: :identifier_scheme).institution + + Org.includes(identifiers: :identifier_scheme).default_orgs) + @orgs = @orgs.flatten.uniq.sort_by(&:name) + @plan.org_id = current_user.org&.id # TODO: is this still used? We cannot switch this to use the :plan_params @@ -45,15 +52,13 @@ def new # query string will fail flash[:notice] = "#{_('This is a')} #{_('test plan')}" if params.key?(:test) @is_test = params[:test] ||= false - - # render :new_plan respond_to :html - end # rubocop:enable Metrics/AbcSize # POST /plans # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def create @plan = Plan.new authorize @plan @@ -63,11 +68,11 @@ def create if plan_params[:template_id].blank? # Something went wrong there should always be a template id respond_to do |format| - flash[:alert] = _("Unable to identify a suitable template for your plan.") + flash[:alert] = _('Unable to identify a suitable template for your plan.') format.html { redirect_to new_plan_path } end else - @plan.visibility = if plan_params["visibility"].blank? + @plan.visibility = if plan_params['visibility'].blank? Rails.configuration.x.plans.default_visibility else plan_params[:visibility] @@ -77,9 +82,9 @@ def create @plan.title = if plan_params[:title].blank? if current_user.firstname.blank? - _("My Plan") + "(" + @plan.template.title + ")" + "#{_('My Plan')}(#{@plan.template.title})" else - current_user.firstname + "'s" + _(" Plan") + "#{current_user.firstname}'s#{_(' Plan')}" end else plan_params[:title] @@ -126,9 +131,7 @@ def create # rubocop:enable Layout/LineLength else # We used the specified org's or funder's template - # rubocop:disable Layout/LineLength msg += " #{_('This plan is based on the')} #{@plan.template.org.name}: '#{@plan.template.title}' template." - # rubocop:enable Layout/LineLength end @plan.add_user!(current_user.id, :creator) @@ -146,23 +149,25 @@ def create else # Something went wrong so report the issue to the user respond_to do |format| - flash[:alert] = failure_message(@plan, _("create")) + flash[:alert] = failure_message(@plan, _('create')) format.html { redirect_to new_plan_path } end end end end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # GET /plans/show # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def show @plan = Plan.includes( template: { phases: { sections: { questions: :answers } } }, plans_guidance_groups: { guidance_group: :guidances } ).find(params[:id]) authorize @plan + @visibility = if @plan.visibility.present? @plan.visibility.to_s else @@ -189,18 +194,16 @@ def show @important_ggs << [current_user.org, @all_ggs_grouped_by_org[current_user.org]] end @all_ggs_grouped_by_org.each do |org, ggs| - @important_ggs << [org, ggs] if Org.default_orgs.include?(org) + @important_ggs << [org, ggs] if !(ggs & @selected_guidance_groups).empty? && !@important_ggs.include?([org, ggs]) # If this is one of the already selected guidance groups its important! - unless (ggs & @selected_guidance_groups).empty? - @important_ggs << [org, ggs] unless @important_ggs.include?([org, ggs]) - end + @important_ggs << [org, ggs] if !(ggs & @selected_guidance_groups).empty? && !@important_ggs.include?([org, ggs]) end # Sort the rest by org name for the accordion - @important_ggs = @important_ggs.sort_by { |org, _gg| (org.nil? ? "" : org.name) } + @important_ggs = @important_ggs.sort_by { |org, _gg| (org.nil? ? '' : org.name) } @all_ggs_grouped_by_org = @all_ggs_grouped_by_org.sort_by do |org, _gg| - (org.nil? ? "" : org.name) + (org.nil? ? '' : org.name) end @selected_guidance_groups = @selected_guidance_groups.ids @@ -215,12 +218,13 @@ def show respond_to :html end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # TODO: This feels like it belongs on a phases controller, perhaps introducing # a non-namespaces phases_controller woulld make sense here. Consider # doing this when we refactor the Plan editing UI # GET /plans/:plan_id/phases/:id/edit + # rubocop:disable Metrics/AbcSize def edit plan = Plan.includes( { template: { @@ -234,7 +238,6 @@ def edit ) .find(params[:id]) authorize plan - phase_id = params[:phase_id].to_i phase = plan.template.phases.select { |p| p.id == phase_id }.first raise ActiveRecord::RecordNotFound if phase.nil? @@ -242,9 +245,10 @@ def edit guidance_groups = GuidanceGroup.where(published: true, id: plan.guidance_group_ids) render_phases_edit(plan, phase, guidance_groups) end + # rubcocop:enable Metrics/AbcSize # PUT /plans/1 - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity def update @plan = Plan.find(params[:id]) authorize @plan @@ -267,35 +271,34 @@ def update funder_attrs[:org_id] = plan_params[:funder][:id] funder = org_from_params(params_in: funder_attrs, allow_create: true) @plan.funder_id = funder&.id + @plan.grant = plan_params[:grant] attrs.delete(:funder) - - process_grant(grant_params: plan_params[:grant]) attrs.delete(:grant) attrs = remove_org_selection_params(params_in: attrs) if @plan.update(attrs) # _attributes(attrs) format.html do redirect_to plan_path(@plan), - notice: success_message(@plan, _("saved")) + notice: success_message(@plan, _('saved')) end format.json do - render json: { code: 1, msg: success_message(@plan, _("saved")) } + render json: { code: 1, msg: success_message(@plan, _('saved')) } end else format.html do # TODO: Should do a `render :show` here instead but show defines too many # instance variables in the controller - redirect_to plan_path(@plan).to_s, alert: failure_message(@plan, _("save")) + redirect_to plan_path(@plan).to_s, alert: failure_message(@plan, _('save')) end format.json do - render json: { code: 0, msg: failure_message(@plan, _("save")) } + render json: { code: 0, msg: failure_message(@plan, _('save')) } end end rescue StandardError => e - flash[:alert] = failure_message(@plan, _("save")) + flash[:alert] = failure_message(@plan, _('save')) format.html do Rails.logger.error "Unable to save plan #{@plan&.id} - #{e.message}" - redirect_to plan_path(@plan).to_s, alert: failure_message(@plan, _("save")) + redirect_to plan_path(@plan).to_s, alert: failure_message(@plan, _('save')) end format.json do render json: { code: 0, msg: flash[:alert] } @@ -303,7 +306,7 @@ def update end # rubocop:enable Metrics/BlockLength end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity # GET /plans/:id/share def share @@ -321,7 +324,6 @@ def share # GET /plans/:id/request_feedback def request_feedback @plan = Plan.find(params[:id]) - if @plan.present? authorize @plan @plan_roles = @plan.roles.where(active: true) @@ -331,6 +333,7 @@ def request_feedback end # DELETE /plans/:id + # rubocop:disable Metrics/AbcSize def destroy @plan = Plan.find(params[:id]) authorize @plan @@ -338,48 +341,50 @@ def destroy respond_to do |format| format.html do redirect_to plans_url, - notice: success_message(@plan, _("deleted")) + notice: success_message(@plan, _('deleted')) end end else respond_to do |format| - flash[:alert] = failure_message(@plan, _("delete")) - format.html { render action: "edit" } + flash[:alert] = failure_message(@plan, _('delete')) + format.html { render action: 'edit' } end end end + # rubocop:enable Metrics/AbcSize # TODO: Is this used? It seems like it belongs on the answers controller # GET /plans/:id/answer + # rubocop:disable Metrics/AbcSize def answer @plan = Plan.find(params[:id]) authorize @plan - if !params[:q_id].nil? + if params[:q_id].nil? respond_to do |format| - format.json do - render json: @plan.answer(params[:q_id], false).to_json(include: :options) - end + format.json { render json: {} } end else respond_to do |format| - format.json { render json: {} } + format.json do + render json: @plan.answer(params[:q_id], false).to_json(include: :options) + end end end end + # rubocop:enable Metrics/AbcSize # GET /plans/:id/download def download @plan = Plan.find(params[:id]) authorize @plan @phase_options = @plan.phases.order(:number).pluck(:title, :id) - if @phase_options.length > 1 - @phase_options.insert(0,["All phases", "All"]) - end + @phase_options.insert(0, ['All phases', 'All']) if @phase_options.length > 1 @export_settings = @plan.settings(:export) - render "download" + render 'download' end - + # POST /plans/:id/duplicate + # rubocop:disable Metrics/AbcSize def duplicate plan = Plan.find(params[:id]) authorize plan @@ -387,12 +392,13 @@ def duplicate respond_to do |format| if @plan.save @plan.add_user!(current_user.id, :creator) - format.html { redirect_to @plan, notice: success_message(@plan, _("copied")) } + format.html { redirect_to @plan, notice: success_message(@plan, _('copied')) } else - format.html { redirect_to plans_path, alert: failure_message(@plan, _("copy")) } + format.html { redirect_to plans_path, alert: failure_message(@plan, _('copy')) } end end end + # rubocop:enable Metrics/AbcSize # TODO: This should probablly just be merged with the update route # POST /plans/:id/visibility @@ -405,29 +411,25 @@ def visibility plan.visibility = plan_params[:visibility] if plan.save deliver_if(recipients: plan.owner_and_coowners, - key: "owners_and_coowners.visibility_changed") do |r| + key: 'owners_and_coowners.visibility_changed') do |r| UserMailer.plan_visibility(r, plan).deliver_now end render status: :ok, - json: { msg: success_message(plan, _("updated")) } + json: { msg: success_message(plan, _('updated')) } else render status: :internal_server_error, - json: { msg: failure_message(plan, _("update")) } + json: { msg: failure_message(plan, _('update')) } end else # rubocop:disable Layout/LineLength render status: :forbidden, json: { - msg: _("Unable to change the plan's status since it is needed at least %{percentage} percentage responded") % { - percentage: Rails.configuration.x.plans.default_percentage_answered - } + msg: format(_("Unable to change the plan's status since it is needed at least %{percentage} percentage responded"), percentage: Rails.configuration.x.plans.default_percentage_answered) } # rubocop:enable Layout/LineLength end else render status: :not_found, - json: { msg: _("Unable to find plan id %{plan_id}") % { - plan_id: params[:id] - } } + json: { msg: format(_('Unable to find plan id %{plan_id}'), plan_id: params[:id]) } end end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength @@ -437,19 +439,17 @@ def visibility def set_test plan = Plan.find(params[:id]) authorize plan - plan.visibility = (params[:is_test] == "1" ? :is_test : :privately_visible) - # rubocop:disable Layout/LineLength + plan.visibility = (params[:is_test] == '1' ? :is_test : :privately_visible) if plan.save render json: { code: 1, - msg: (plan.is_test? ? _("Your project is now a test.") : _("Your project is no longer a test.")) + msg: (plan.is_test? ? _('Your project is now a test.') : _('Your project is no longer a test.')) } else render status: :bad_request, json: { code: 0, msg: _("Unable to change the plan's test status") } end - # rubocop:enable Layout/LineLength end # GET /plans/:id/overview @@ -460,9 +460,7 @@ def overview authorize plan render(:overview, locals: { plan: plan }) rescue ActiveRecord::RecordNotFound - flash[:alert] = _("There is no plan associated with id %{id}") % { - id: params[:id] - } + flash[:alert] = format(_('There is no plan associated with id %{s'), id: params[:id]) redirect_to(action: :index) end @@ -493,11 +491,11 @@ def get_most_recent(templates) groups = {} templates.each do |t| k = t.family_id - if !groups.key?(k) - groups[k] = t - else + if groups.key?(k) other = groups[k] groups[k] = t if other.version < t.version + else + groups[k] = t end end groups.values @@ -518,7 +516,7 @@ def rollup(plan, src_plan_key, super_id, obj_plan_key) end plan[obj_plan_key].each do |o| - id = o["id"] + id = o['id'] o[src_plan_key] = id_to_obj[id] if id_to_obj.key?(id) end plan.delete(src_plan_key) @@ -528,8 +526,8 @@ def render_phases_edit(plan, phase, guidance_groups) readonly = !plan.editable_by?(current_user.id) # Since the answers have been pre-fetched through plan (see Plan.load_for_phase) # we create a hash whose keys are question id and value is the answer associated - answers = plan.answers.each_with_object({}) { |a, m| m[a.question_id] = a; } - render("/phases/edit", locals: { + answers = plan.answers.each_with_object({}) { |a, m| m[a.question_id] = a } + render('/phases/edit', locals: { base_template_org: phase.template.base_org, plan: plan, phase: phase, @@ -540,34 +538,11 @@ def render_phases_edit(plan, phase, guidance_groups) }) end - # Update, destroy or add the grant - def process_grant(grant_params:) - return false unless grant_params.present? - - grant = @plan.grant - - # delete it if it has been blanked out - if grant_params[:value].blank? && grant.present? - grant.destroy - @plan.grant = nil - elsif grant_params[:value] != grant&.value - if grant.present? - grant.update(value: grant_params[:value]) - elsif grant_params[:value].present? - @plan.grant = Identifier.new(identifier_scheme: nil, identifiable: @plan, - value: grant_params[:value]) - end - end - end - # rubocop:enable - def setup_local_orgs @orgs = (Org.includes(identifiers: :identifier_scheme).organisation + Org.includes(identifiers: :identifier_scheme).institution + Org.includes(identifiers: :identifier_scheme).default_orgs) - @orgs = @orgs.flatten.uniq.sort_by(&:name) - + @orgs = @orgs.flatten.uniq.sort_by(&:name) end - end # rubocop:enable Metrics/ClassLength diff --git a/app/controllers/public_pages_controller.rb b/app/controllers/public_pages_controller.rb index a97ca88279..3a19449b73 100644 --- a/app/controllers/public_pages_controller.rb +++ b/app/controllers/public_pages_controller.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true +# Controller for the Public DMPs and Funder Requirements pages class PublicPagesController < ApplicationController - # GET template_index # ----------------------------------------------------- # rubocop:disable Metrics/AbcSize def template_index @templates_query_params = { page: paginable_params.fetch(:page, 1), - search: paginable_params.fetch(:search, ""), - sort_field: paginable_params.fetch(:sort_field, "templates.title"), - sort_direction: paginable_params.fetch(:sort_direction, "asc") + search: paginable_params.fetch(:search, ''), + sort_field: paginable_params.fetch(:sort_field, 'templates.title'), + sort_direction: paginable_params.fetch(:sort_direction, 'asc') } templates = Template.live(Template.families(Org.funder.pluck(:id)).pluck(:family_id)) @@ -30,8 +30,8 @@ def template_export @template = Template.live(params[:id]) # covers authorization for this action. # Pundit dosent support passing objects into scoped policies - unless PublicPagePolicy.new(@template).template_export? - msg = "You are not authorized to export that template" + unless PublicPagePolicy.new(current_user, @template).template_export? + msg = 'You are not authorized to export that template' redirect_to public_templates_path, notice: msg and return # raise Pundit::NotAuthorizedError end @@ -52,35 +52,32 @@ def template_export @formatting = Settings::Template::DEFAULT_SETTINGS[:formatting] begin - file_name = @template.title.gsub(/[^a-zA-Z\d\s]/, "").gsub(/ /, "_") + file_name = @template.title.gsub(/[^a-zA-Z\d\s]/, '').gsub(/ /, '_') file_name = "#{file_name}_v#{@template.version}" respond_to do |format| format.docx do - render docx: "template_exports/template_export", filename: "#{file_name}.docx" + render docx: 'template_exports/template_export', filename: "#{file_name}.docx" end format.pdf do - # rubocop:disable Layout/LineLength render pdf: file_name, - template: "template_exports/template_export", - margin: @formatting[:margin], - footer: { - center: _("Template created using the %{application_name} service. Last modified %{date}") % { - application_name: ApplicationService.application_name, - date: l(@template.updated_at.to_date, formats: :short) - }, - font_size: 8, - spacing: (@formatting[:margin][:bottom] / 2) - 4, - right: _("[page] of [topage]"), - encoding: "utf8" - } - # rubocop:enable Metrics/LineLength + template: 'template_exports/template_export', + margin: @formatting[:margin], + footer: { + center: format(_('Template created using the %{application_name} service. Last modified %{date}'), + application_name: ApplicationService.application_name, + date: l(@template.updated_at.to_date, formats: :short)), + font_size: 8, + spacing: (@formatting[:margin][:bottom] / 2) - 4, + right: '[page] of [topage]', + encoding: 'utf8' + } end end rescue ActiveRecord::RecordInvalid # What scenario is this triggered in? it's common to our export pages redirect_to public_templates_path, - alert: _("Unable to download the DMP Template at this time.") + alert: _('Unable to download the DMP Template at this time.') end end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength @@ -89,12 +86,12 @@ def template_export # ------------------------------------------------------------------------------------ def plan_index @plans = Plan.publicly_visible.includes(:template) - render "plan_index", locals: { + render 'plan_index', locals: { query_params: { page: paginable_params.fetch(:page, 1), - search: paginable_params.fetch(:search, ""), - sort_field: paginable_params.fetch(:sort_field, "plans.updated_at"), - sort_direction: paginable_params.fetch(:sort_direction, "desc") + search: paginable_params.fetch(:search, ''), + sort_field: paginable_params.fetch(:sort_field, 'plans.updated_at'), + sort_direction: paginable_params.fetch(:sort_direction, 'desc') } } end @@ -104,5 +101,4 @@ def plan_index def paginable_params params.permit(:page, :search, :sort_field, :sort_direction) end - end diff --git a/app/controllers/question_formats_controller.rb b/app/controllers/question_formats_controller.rb index 29356bf9d1..9cf5c63ded 100644 --- a/app/controllers/question_formats_controller.rb +++ b/app/controllers/question_formats_controller.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true +# Controller that handles the RDA Metadata question type class QuestionFormatsController < ApplicationController - # do we need authorizaton on this? it will only return the URL for the rda api # down the line we will add more methods for other external api's def rda_api_address render json: { - "url": QuestionFormat.rda_metadata.first.description + url: QuestionFormat.rda_metadata.first.description }.to_json end - end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 3ab9b7e05d..0f3cd3a20e 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,69 +1,66 @@ # frozen_string_literal: true +# Controller that handles user account creation and changes from the edit profile page class RegistrationsController < Devise::RegistrationsController - include OrgSelectable def edit @user = current_user @prefs = @user.get_preferences(:email) @languages = Language.sorted_by_abbreviation - @orgs = Org.order("name") + @orgs = Org.order('name') @other_organisations = Org.where(is_other: true).pluck(:id) @identifier_schemes = IdentifierScheme.for_users.order(:name) @default_org = current_user.org - msg = "No default preferences found (should be in dmproadmap.rb initializer)." + msg = 'No default preferences found (should be in dmproadmap.rb initializer).' flash[:alert] = msg unless @prefs end # GET /resource + # rubocop:disable Metrics/AbcSize def new oauth = { provider: nil, uid: nil } IdentifierScheme.for_users.each do |scheme| - unless session["devise.#{scheme.name.downcase}_data"].nil? - oauth = session["devise.#{scheme.name.downcase}_data"] - end + oauth = session["devise.#{scheme.name.downcase}_data"] unless session["devise.#{scheme.name.downcase}_data"].nil? end @user = User.new # no oath, no provider or no uid - bail out - return if oauth.nil? or oauth["provider"].nil? or oauth["uid"].nil? + return if oauth.nil? || oauth['provider'].nil? || oauth['uid'].nil? # Connect the new user with the identifier sent back by the OAuth provider - flash[:notice] = _("Please make a choice below. After linking your + flash[:notice] = format(_("Please make a choice below. After linking your details to a %{application_name} account, you will be able to sign in directly with your - institutional credentials.") % { - application_name: ApplicationService.application_name - } + institutional credentials."), application_name: ApplicationService.application_name) end + # rubocop:enable Metrics/AbcSize - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockNesting, Layout/LineLength + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity # POST /resource def create oauth = { provider: nil, uid: nil } IdentifierScheme.for_users.each do |scheme| - unless session["devise.#{scheme.name.downcase}_data"].nil? - oauth = session["devise.#{scheme.name.downcase}_data"] - end + oauth = session["devise.#{scheme.name.downcase}_data"] unless session["devise.#{scheme.name.downcase}_data"].nil? end blank_org = if Rails.configuration.x.application.restrict_orgs - sign_up_params[:org_id]["id"].blank? + sign_up_params[:org_id]['id'].blank? else sign_up_params[:org_id].blank? end - if sign_up_params[:accept_terms].to_s == "0" + if sign_up_params[:accept_terms].to_s == '0' redirect_to after_sign_up_error_path_for(resource), - alert: _("You must accept the terms and conditions to register.") + alert: _('You must accept the terms and conditions to register.') elsif blank_org redirect_to after_sign_up_error_path_for(resource), - alert: _("Please select an organisation from the list, or choose Other.") + alert: _('Please select an organisation from the list, or choose Other.') else - existing_user = User.where_case_insensitive("email", sign_up_params[:email]).first + existing_user = User.where_case_insensitive('email', sign_up_params[:email]).first if existing_user.present? if existing_user.invitation_token.present? && !existing_user.accept_terms? # If the user is creating an account but they have an outstanding invitation, remember @@ -74,7 +71,7 @@ def create existing_user.destroy else redirect_to after_sign_up_error_path_for(resource), - alert: _("That email address is already registered.") + alert: _('That email address is already registered.') return end end @@ -102,23 +99,22 @@ def create # Determine if reCAPTCHA is enabled and if so verify it use_recaptcha = Rails.configuration.x.recaptcha.enabled || false if (!use_recaptcha || verify_recaptcha(model: resource)) && resource.save + # rubocop:disable Metrics/BlockNesting if resource.active_for_authentication? set_flash_message :notice, :signed_up if is_navigational_format? sign_up(resource_name, resource) UserMailer.welcome_notification(current_user).deliver_now - unless oauth.nil? - # The OAuth provider could not be determined or there was no unique UID! - unless oauth["provider"].nil? || oauth["uid"].nil? - prov = IdentifierScheme.find_by(name: oauth["provider"].downcase) - # Until we enable ORCID signups - if prov.present? && prov.name == "shibboleth" - Identifier.create(identifier_scheme: prov, - value: oauth["uid"], - attrs: oauth, - identifiable: resource) - flash[:notice] = _("Welcome! You have signed up successfully with your institutional credentials. You will now be able to access your account with them.") - # rubocop:enable Layout/LineLength - end + if !oauth.nil? && !(oauth['provider'].nil? || oauth['uid'].nil?) + prov = IdentifierScheme.find_by(name: oauth['provider'].downcase) + # Until we enable ORCID signups + if prov.present? && prov.name == 'shibboleth' + Identifier.create(identifier_scheme: prov, + value: oauth['uid'], + attrs: oauth, + identifiable: resource) + flash[:notice] = _('Welcome! You have signed up successfully with your + institutional credentials. You will now be able to access + your account with them.') end end respond_with resource, location: after_sign_up_path_for(resource) @@ -126,35 +122,37 @@ def create set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" respond_with resource, location: after_inactive_sign_up_path_for(resource) end + # rubocop:enable Metrics/BlockNesting else clean_up_passwords resource redirect_to after_sign_up_error_path_for(resource), - alert: _("Unable to create your account.%{errors_for_display}") % { - errors_for_display: errors_for_display(resource) - } - # rubocop:enable Metrics/LineLength + alert: _("Unable to create your account.#{errors_for_display(resource)}") + end end end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockNesting + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity + # rubocop:disable Metrics/AbcSize def update if user_signed_in? @prefs = @user.get_preferences(:email) - @orgs = Org.order("name") + @orgs = Org.order('name') @default_org = current_user.org @other_organisations = Org.where(is_other: true).pluck(:id) @identifier_schemes = IdentifierScheme.for_users.order(:name) @languages = Language.sorted_by_abbreviation - if params[:skip_personal_details] == "true" + if params[:skip_personal_details] == 'true' do_update_password(current_user, update_params) else do_update(needs_password?(current_user)) end else - render(file: File.join(Rails.root, "public/403.html"), status: 403, layout: false) + render(file: File.join(Rails.root, 'public/403.html'), status: 403, layout: false) end end + # rubocop:enable Metrics/AbcSize private @@ -165,27 +163,29 @@ def needs_password?(user) user.email != update_params[:email] || update_params[:password].present? end - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Layout/LineLength, Metrics/BlockNesting + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # rubocop:disable Style/OptionalBooleanParameter def do_update(require_password = true, confirm = false) restrict_orgs = Rails.configuration.x.application.restrict_orgs mandatory_params = true # added to by below, overwritten otherwise - message = _("Save Unsuccessful. ") + message = _('Save Unsuccessful. ') # ensure that the required fields are present if update_params[:email].blank? - message += _("Please enter an email address. ") + message += _('Please enter an email address. ') mandatory_params &&= false end if update_params[:firstname].blank? - message += _("Please enter a First name. ") + message += _('Please enter a First name. ') mandatory_params &&= false end if update_params[:surname].blank? - message += _("Please enter a Last name. ") + message += _('Please enter a Last name. ') mandatory_params &&= false end - if restrict_orgs && update_params[:org_id]["id"].blank? + if restrict_orgs && update_params[:org_id]['id'].blank? message += _("Please select an organisation from the list, or enter your organisation's name.") mandatory_params &&= false end @@ -199,22 +199,7 @@ def do_update(require_password = true, confirm = false) # user is changing email or password if require_password # if user is changing email - if current_user.email != attrs[:email] - # password needs to be present - if attrs[:password].blank? - message = _("Please enter your password to change email address.") - successfully_updated = false - elsif current_user.valid_password?(attrs[:current_password]) - successfully_updated = current_user.update_with_password(attrs) - unless successfully_updated - message = _("Save unsuccessful. \ - That email address is already registered. \ - You must enter a unique email address.") - end - else - message = _("Invalid password") - end - else + if current_user.email == attrs[:email] # remove the current_password because its not actuallyt part of the User record attrs.delete(:current_password) @@ -222,6 +207,21 @@ def do_update(require_password = true, confirm = false) # require_password = true is because the email changed. # The case for password changed goes to do_update_password instead successfully_updated = current_user.update_without_password(attrs) + elsif attrs[:password].blank? + # password needs to be present + message = _('Please enter your password to change email address.') + successfully_updated = false + elsif current_user.valid_password?(attrs[:current_password]) + successfully_updated = current_user.update_with_password(attrs) + # rubocop:disable Metrics/BlockNesting + unless successfully_updated + message = _("Save unsuccessful. \ + That email address is already registered. \ + You must enter a unique email address.") + end + # rubocop:enable Metrics/BlockNesting + else + message = _('Invalid password') end else # password not required @@ -234,7 +234,7 @@ def do_update(require_password = true, confirm = false) end # unlink shibboleth from user's details - current_user.update_attributes(shibboleth_id: "") if params[:unlink_flag] == "true" + current_user.update_attributes(shibboleth_id: '') if params[:unlink_flag] == 'true' # render the correct page if successfully_updated @@ -246,28 +246,30 @@ def do_update(require_password = true, confirm = false) session[:locale] = current_user.locale unless current_user.locale.nil? # Method defined at controllers/application_controller.rb set_locale - set_flash_message :notice, success_message(current_user, _("saved")) + set_flash_message :notice, success_message(current_user, _('saved')) # Sign in the user bypassing validation in case his password changed sign_in current_user, bypass: true - redirect_to "#{edit_user_registration_path}\#personal-details", - notice: success_message(current_user, _("saved")) + redirect_to "#{edit_user_registration_path}#personal-details", + notice: success_message(current_user, _('saved')) else - flash[:alert] = message.blank? ? failure_message(current_user, _("save")) : message - @orgs = Org.order("name") - render "edit" + flash[:alert] = message.blank? ? failure_message(current_user, _('save')) : message + @orgs = Org.order('name') + render 'edit' end end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Layout/LineLength, Metrics/BlockNesting + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # rubocop:enable Style/OptionalBooleanParameter - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity def do_update_password(current_user, args) if args[:current_password].blank? - message = _("Please enter your current password") + message = _('Please enter your current password') elsif args[:password_confirmation].blank? - message = _("Please enter a password confirmation") + message = _('Please enter a password confirmation') elsif args[:password] != args[:password_confirmation] - message = _("Password and comfirmation must match") + message = _('Password and comfirmation must match') else successfully_updated = current_user.update_with_password(args) end @@ -275,18 +277,18 @@ def do_update_password(current_user, args) if successfully_updated session[:locale] = current_user.locale unless current_user.locale.nil? # Method defined at controllers/application_controller.rb#set_locale - set_flash_message :notice, success_message(current_user, _("saved")) + set_flash_message :notice, success_message(current_user, _('saved')) # TODO: this method is deprecated sign_in current_user, bypass: true - redirect_to "#{edit_user_registration_path}\#password-details", - notice: success_message(current_user, _("saved")) + redirect_to "#{edit_user_registration_path}#password-details", + notice: success_message(current_user, _('saved')) else - flash[:alert] = message.blank? ? failure_message(current_user, _("save")) : message - redirect_to "#{edit_user_registration_path}\#password-details" + flash[:alert] = message.blank? ? failure_message(current_user, _('save')) : message + redirect_to "#{edit_user_registration_path}#password-details" end end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity def sign_up_params params.require(:user).permit(:email, :password, :password_confirmation, @@ -306,7 +308,7 @@ def update_params def handle_org(attrs:) return attrs unless attrs.present? && attrs[:org_id].present? - org = org_from_params(params_in: attrs, allow_create: true) + org = org_from_params(params_in: attrs) # Remove the extraneous Org Selector hidden fields attrs = remove_org_selection_params(params_in: attrs) @@ -316,5 +318,4 @@ def handle_org(attrs:) attrs[:org_id] = org.id attrs end - end diff --git a/app/controllers/research_outputs_controller.rb b/app/controllers/research_outputs_controller.rb new file mode 100644 index 0000000000..c05927b20c --- /dev/null +++ b/app/controllers/research_outputs_controller.rb @@ -0,0 +1,219 @@ +# frozen_string_literal: true + +# Controller to handle CRUD operations for the Research Outputs tab +class ResearchOutputsController < ApplicationController + helper PaginableHelper + + before_action :fetch_plan, except: %i[select_output_type select_license repository_search + metadata_standard_search] + before_action :fetch_research_output, only: %i[edit update destroy] + + after_action :verify_authorized + + # GET /plans/:plan_id/research_outputs + def index + @research_outputs = ResearchOutput.includes(:repositories) + .where(plan_id: @plan.id) + authorize @research_outputs.first || ResearchOutput.new(plan_id: @plan.id) + end + + # GET /plans/:plan_id/research_outputs/new + def new + @research_output = ResearchOutput.new(plan_id: @plan.id, output_type: '') + authorize @research_output + end + + # GET /plans/:plan_id/research_outputs/:id/edit + def edit + authorize @research_output + end + + # POST /plans/:plan_id/research_outputs + def create + args = process_byte_size.merge({ plan_id: @plan.id }) + args = process_nillable_values(args: args) + @research_output = ResearchOutput.new(args) + authorize @research_output + + if @research_output.save + redirect_to plan_research_outputs_path(@plan), + notice: success_message(@research_output, _('added')) + else + flash[:alert] = failure_message(@research_output, _('add')) + render 'research_outputs/new' + end + end + + # PATCH/PUT /plans/:plan_id/research_outputs/:id + # rubocop:disable Metrics/AbcSize + def update + args = process_byte_size.merge({ plan_id: @plan.id }) + args = process_nillable_values(args: args) + authorize @research_output + + # Clear any existing repository and metadata_standard selections. + @research_output.repositories.clear + @research_output.metadata_standards.clear + + if @research_output.update(args) + redirect_to plan_research_outputs_path(@plan), + notice: success_message(@research_output, _('saved')) + else + redirect_to edit_plan_research_output_path(@plan, @research_output), + alert: failure_message(@research_output, _('save')) + end + end + # rubocop:enable Metrics/AbcSize + + # DELETE /plans/:plan_id/research_outputs/:id + def destroy + authorize @research_output + + if @research_output.destroy + redirect_to plan_research_outputs_path(@plan), + notice: success_message(@research_output, _('removed')) + else + redirect_to plan_research_outputs_path(@plan), + alert: failure_message(@research_output, _('remove')) + end + end + + # ============================ + # = Rails UJS remote methods = + # ============================ + + # GET /plans/:id/output_type_selection + def select_output_type + @plan = Plan.find_by(id: params[:plan_id]) + @research_output = ResearchOutput.new( + plan: @plan, output_type: output_params[:output_type] + ) + authorize @research_output + end + + # GET /plans/:id/license_selection + def select_license + @plan = Plan.find_by(id: params[:plan_id]) + @research_output = ResearchOutput.new( + plan: @plan, license_id: output_params[:license_id] + ) + authorize @research_output + end + + # GET /plans/:id/repository_search + # rubocop:disable Metrics/AbcSize + def repository_search + @plan = Plan.find_by(id: params[:plan_id]) + @research_output = ResearchOutput.new(plan: @plan) + authorize @research_output + + @search_results = Repository.by_type(repo_search_params[:type_filter]) + @search_results = @search_results.by_subject(repo_search_params[:subject_filter]) + @search_results = @search_results.search(repo_search_params[:search_term]) + + @search_results = @search_results.order(:name).page(params[:page]) + end + # rubocop:enable Metrics/AbcSize + + # PUT /plans/:id/repository_select + def repository_select + @plan = Plan.find_by(id: params[:plan_id]) + @research_output = ResearchOutput.new(plan: @plan) + authorize @research_output + + @research_output + end + + # PUT /plans/:id/repository_unselect + def repository_unselect + @plan = Plan.find_by(id: params[:plan_id]) + @research_output = ResearchOutput.new(plan: @plan) + authorize @research_output + end + + # GET /plans/:id/metadata_standard_search + def metadata_standard_search + @plan = Plan.find_by(id: params[:plan_id]) + @research_output = ResearchOutput.new(plan: @plan) + authorize @research_output + + @search_results = MetadataStandard.search(metadata_standard_search_params[:search_term]) + .order(:title) + .page(params[:page]) + end + + private + + def output_params + params.require(:research_output) + .permit(%i[title abbreviation description output_type output_type_description + sensitive_data personal_data file_size file_size_unit mime_type_id + release_date access coverage_start coverage_end coverage_region + mandatory_attribution license_id], + repositories_attributes: %i[id], metadata_standards_attributes: %i[id]) + end + + def repo_search_params + params.require(:research_output).permit(%i[search_term subject_filter type_filter]) + end + + def metadata_standard_search_params + params.require(:research_output).permit(%i[search_term]) + end + + # rubocop:disable Metrics/AbcSize + def process_byte_size + args = output_params + + if args[:file_size].present? + byte_size = 0.bytes + case args[:file_size_unit] + when 'pb' + args[:file_size].to_f.petabytes + when 'tb' + args[:file_size].to_f.terabytes + when 'gb' + args[:file_size].to_f.gigabytes + when 'mb' + args[:file_size].to_f.megabytes + else + args[:file_size].to_i + end + + args[:byte_size] = byte_size + end + + args.delete(:file_size) + args.delete(:file_size_unit) + args + end + # rubocop:enable Metrics/AbcSize + + # There are certain fields on the form that are visible based on the selected output_type. If the + # ResearchOutput previously had a value for any of these and the output_type then changed making + # one of these arguments invisible, then we need to blank it out here since the Rails form will + # not send us the value + def process_nillable_values(args:) + args[:byte_size] = nil unless args[:byte_size].present? + args + end + + # ============= + # = Callbacks = + # ============= + + def fetch_plan + @plan = Plan.find_by(id: params[:plan_id]) + return true if @plan.present? + + redirect_to root_path, alert: _('plan not found') + end + + def fetch_research_output + @research_output = ResearchOutput.includes(:repositories) + .find_by(id: params[:id]) + return true if @research_output.present? && + @plan.research_outputs.include?(@research_output) + + redirect_to plan_research_outputs_path, alert: _('research output not found') + end +end diff --git a/app/controllers/research_projects_controller.rb b/app/controllers/research_projects_controller.rb index 0792cf2f36..68483b4596 100644 --- a/app/controllers/research_projects_controller.rb +++ b/app/controllers/research_projects_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Controller for Grant typeahead class ResearchProjectsController < ApplicationController - def index render json: research_projects end @@ -19,7 +19,7 @@ def research_projects # Check the cache contents as well since the instance variable is only # relevant per request - cached = Rails.cache.fetch(["research_projects", funder_type]) + cached = Rails.cache.fetch(['research_projects', funder_type]) return @research_projects = cached unless cached.nil? || cached.empty? @research_projects = fetch_projects @@ -30,7 +30,7 @@ def funder_type end def fetch_projects - Rails.cache.fetch(["research_projects", funder_type], expires_in: expiry) do + Rails.cache.fetch(['research_projects', funder_type], expires_in: expiry) do ExternalApis::OpenAireService.search(funder: funder_type) end end @@ -40,5 +40,4 @@ def expiry expiration = Rails.configuration.x.cache.research_projects_expiration expiration.present? ? expiration : 1.day end - end diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index 58e583eb65..bf0fe082df 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -1,14 +1,15 @@ # frozen_string_literal: true +# Controller that handles adding/updating/removing collaborators from a plan class RolesController < ApplicationController - include ConditionalUserMailer respond_to :html after_action :verify_authorized # POST /roles - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/BlockNesting + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def create registered = true @@ -16,101 +17,100 @@ def create @role = Role.new(plan: plan, access: role_params[:access]) authorize @role - message = "" + message = '' if role_params[:user].present? && role_params[:user].key?(:email) && role_params[:user][:email].present? && plan.present? if @role.plan.owner.present? && @role.plan.owner.email == role_params[:user][:email] - # rubocop:disable Layout/LineLength - flash[:notice] = _("Cannot share plan with %{email} since that email matches with the owner of the plan.") % { - email: role_params[:user][:email] - } - # rubocop:enable Layout/LineLength + flash[:notice] = format(_('Cannot share plan with %{email} since that email matches + with the owner of the plan.'), + email: role_params[:user][:email]) else - user = User.where_case_insensitive("email", role_params[:user][:email]).first + user = User.where_case_insensitive('email', role_params[:user][:email]).first if user.present? && Role.where(plan: @role.plan, user: user, active: true) .count .positive? # role already exists - flash[:notice] = _("Plan is already shared with %{email}.") % { - email: role_params[:user][:email] - } + flash[:notice] = format(_('Plan is already shared with %{email}.'), + email: role_params[:user][:email]) else + # rubocop:disable Metrics/BlockNesting if user.nil? registered = false User.invite!({ email: role_params[:user][:email], - firstname: _("First Name"), - surname: _("Surname"), + firstname: _('First Name'), + surname: _('Surname'), org: current_user.org }, current_user) - message = _("Invitation to %{email} issued successfully.") % { - email: role_params[:user][:email] - } - user = User.where_case_insensitive("email", role_params[:user][:email]).first + message = format(_('Invitation to %{email} issued successfully.'), + email: role_params[:user][:email]) + user = User.where_case_insensitive('email', role_params[:user][:email]).first end - message += _("Plan shared with %{email}.") % { - email: user.email - } + + message += format(_('Plan shared with %{email}.'), email: user.email) @role.user = user if @role.save if registered - deliver_if(recipients: user, key: "users.added_as_coowner") do |r| + deliver_if(recipients: user, key: 'users.added_as_coowner') do |r| UserMailer.sharing_notification(@role, r, inviter: current_user) .deliver_now end end flash[:notice] = message else - # rubocop:disable Layout/LineLength - flash[:alert] = _("You must provide a valid email address and select a permission level.") - # rubocop:enable Layout/LineLength + flash[:alert] = _('You must provide a valid email address and select a permission + level.') end + # rubocop:enable Metrics/BlockNesting end end else - flash[:alert] = _("Please enter an email address") + flash[:alert] = _('Please enter an email address') end - redirect_to controller: "plans", action: "share", id: @role.plan.id + redirect_to controller: 'plans', action: 'share', id: @role.plan.id end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/BlockNesting - # rubocop:enable + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # PUT /roles/:id + # rubocop:disable Metrics/AbcSize def update @role = Role.find(params[:id]) authorize @role if @role.update_attributes(access: role_params[:access]) - deliver_if(recipients: @role.user, key: "users.added_as_coowner") do |_r| + deliver_if(recipients: @role.user, key: 'users.added_as_coowner') do |_r| UserMailer.permissions_change_notification(@role, current_user).deliver_now end - # rubocop:disable Layout/LineLength render json: { code: 1, - msg: _("Successfully changed the permissions for %{email}. They have been notified via email.") % { email: @role.user.email } + msg: format(_('Successfully changed the permissions for %{email}. They have been + notified via email.'), email: @role.user.email) } - # rubocop:enable Layout/LineLength else render json: { code: 0, msg: flash[:alert] } end end + # rubocop:enable Metrics/AbcSize # DELETE /roles/:id + # rubocop:disable Metrics/AbcSize def destroy @role = Role.find(params[:id]) authorize @role user = @role.user plan = @role.plan @role.destroy - flash[:notice] = _("Access removed") - deliver_if(recipients: user, key: "users.added_as_coowner") do |_r| + flash[:notice] = _('Access removed') + deliver_if(recipients: user, key: 'users.added_as_coowner') do |_r| UserMailer.plan_access_removed(user, plan, current_user).deliver_now end - redirect_to controller: "plans", action: "share", id: @role.plan.id + redirect_to controller: 'plans', action: 'share', id: @role.plan.id end + # rubocop:enable Metrics/AbcSize # This function makes user's role on a plan inactive # i.e. "removes" this from their plans @@ -119,9 +119,9 @@ def deactivate role = Role.find(params[:id]) authorize role if role.deactivate! - flash[:notice] = _("Plan removed") + flash[:notice] = _('Plan removed') else - flash[:alert] = _("Unable to remove the plan") + flash[:alert] = _('Unable to remove the plan') end redirect_to(plans_path) end @@ -131,5 +131,4 @@ def deactivate def role_params params.require(:role).permit(:plan_id, :access, user: %i[email]) end - end diff --git a/app/controllers/session_locales_controller.rb b/app/controllers/session_locales_controller.rb index ac5aa9a0e8..e5ef337f0c 100644 --- a/app/controllers/session_locales_controller.rb +++ b/app/controllers/session_locales_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Controller that handles a language change class SessionLocalesController < ApplicationController - def update session[:locale] = params[:locale] if available_locales.include?(params[:locale].intern) redirect_back(fallback_location: root_path) @@ -12,5 +12,4 @@ def update def available_locales I18n.available_locales end - end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index f6dae60242..04e15f1de2 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,58 +1,7 @@ # frozen_string_literal: true -## TODO verify functionality after merging -## Specifically, set_gettext_locale -# class SessionsController < Devise::SessionsController - -# def new -# redirect_to(root_path) -# end - -# # Capture the user's shibboleth id if they're coming in from an IDP -# # --------------------------------------------------------------------- -# def create -# existing_user = User.find_by(email: params[:user][:email]) -# if !existing_user.nil? - -# # Until ORCID login is supported -# if !session["devise.shibboleth_data"].nil? -# args = { -# identifier_scheme: IdentifierScheme.find_by(name: "shibboleth"), -# identifier: session["devise.shibboleth_data"]["uid"], -# user: existing_user -# } -# if UserIdentifier.create(args) -# # rubocop:disable Metrics/LineLength -# success = _("Your account has been successfully linked to your institutional credentials. You will now be able to sign in with them.") -# # rubocop:enable Metrics/LineLength -# end -# end -# if session[:locale].blank? && existing_user.get_locale.present? -# session[:locale] = existing_user.get_locale -# end -# # Method defined at controllers/application_controller.rb -# set_gettext_locale -# end -# super -# if success -# flash[:notice] = success -# end -# end - -# def destroy -# # We want to keep the user selected language even after the user logs out -# session_locale = session[:locale] -# super -# session[:locale] = session_locale -# # Method defined at controllers/application_controller.rb -# set_gettext_locale -# end - -# end -# frozen_string_literal: true - +# Controller that handles user login and logout class SessionsController < Devise::SessionsController - def new redirect_to(root_path) end @@ -65,12 +14,12 @@ def create unless existing_user.nil? # Until ORCID login is supported - unless session["devise.shibboleth_data"].nil? + unless session['devise.shibboleth_data'].nil? args = { - identifier_scheme: IdentifierScheme.find_by(name: "shibboleth"), - value: session["devise.shibboleth_data"]["uid"], + identifier_scheme: IdentifierScheme.find_by(name: 'shibboleth'), + value: session['devise.shibboleth_data']['uid'], identifiable: existing_user, - attrs: session["devise.shibboleth_data"] + attrs: session['devise.shibboleth_data'] } @ui = Identifier.new(args) end @@ -82,7 +31,7 @@ def create super do if !@ui.nil? && @ui.save # rubocop:disable Layout/LineLength - flash[:notice] = _("Your account has been successfully linked to your institutional credentials. You will now be able to sign in with them.") + flash[:notice] = _('Your account has been successfully linked to your institutional credentials. You will now be able to sign in with them.') # rubocop:enable Layout/LineLength end end @@ -90,13 +39,9 @@ def create # rubocop:enable Metrics/AbcSize def destroy - session_locale = session[:locale] super - # We want to keep the locale even after the user logs out. This way we will - # not keep a default language - session[:locale] = session_locale + session[:locale] = nil # Method defined at controllers/application_controller.rb set_locale end - end diff --git a/app/controllers/settings.rb b/app/controllers/settings.rb index de3ffd121a..c65eeb625a 100644 --- a/app/controllers/settings.rb +++ b/app/controllers/settings.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true +# Controller for the user email preferences module Settings - class SettingsController < ApplicationController - end - end diff --git a/app/controllers/settings/plans_controller.rb b/app/controllers/settings/plans_controller.rb index 77bdff779c..b67fc7154a 100644 --- a/app/controllers/settings/plans_controller.rb +++ b/app/controllers/settings/plans_controller.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true module Settings - + # Controller that handles the download options (e.g. font face, size) for PDF downloads class PlansController < SettingsController - before_action :retrieve_settings after_action :verify_authorized @@ -17,14 +16,14 @@ def show end end - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def update authorize @plan # If this is actually used we should consider switching these to strong params export_params = params[:export].try(:deep_symbolize_keys) settings = @plan.super_settings(:export).tap do |s| - if params[:commit] == "Reset" + if params[:commit] == 'Reset' s.formatting = nil s.fields = nil s.title = nil @@ -36,9 +35,9 @@ def update end if settings.save - flash[:notice] = _("Export settings updated successfully.") + flash[:notice] = _('Export settings updated successfully.') else - flash[:alert] = _("An error has occurred while saving/resetting your export settings.") + flash[:alert] = _('An error has occurred while saving/resetting your export settings.') end respond_to do |format| @@ -47,7 +46,7 @@ def update # format.json { render json: settings_json } end end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength private @@ -64,7 +63,5 @@ def settings_json def plan @plan ||= Plan.find(params[:id]) end - end - end diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb index 8a3a138616..f15c27d876 100644 --- a/app/controllers/static_pages_controller.rb +++ b/app/controllers/static_pages_controller.rb @@ -1,20 +1,14 @@ # frozen_string_literal: true +# Controller that handles requests for static pages class StaticPagesController < ApplicationController + def about_us; end - def about_us - end + def contact_us; end - def contact_us - end + def privacy; end - def privacy - end - - def termsuse - end - - def help - end + def termsuse; end + def help; end end diff --git a/app/controllers/super_admin/api_clients_controller.rb b/app/controllers/super_admin/api_clients_controller.rb index d483020731..591cd93cf9 100644 --- a/app/controllers/super_admin/api_clients_controller.rb +++ b/app/controllers/super_admin/api_clients_controller.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true module SuperAdmin - + # Controller for managing ApiClients for API V1 class ApiClientsController < ApplicationController - respond_to :html include OrgSelectable @@ -29,6 +28,7 @@ def edit end # POST /api_clients + # rubocop:disable Metrics/AbcSize def create authorize(ApiClient) @@ -41,19 +41,20 @@ def create if @api_client.save UserMailer.api_credentials(@api_client).deliver_now - msg = success_message(@api_client, _("created")) - msg += _(". The API credentials have been emailed to %{email}") % { - email: @api_client.contact_email - } + msg = success_message(@api_client, _('created')) + msg += format(_('. The API credentials have been emailed to %{email}'), + email: @api_client.contact_email) flash.now[:notice] = msg render :edit else - flash.now[:alert] = failure_message(@api_client, _("create")) + flash.now[:alert] = failure_message(@api_client, _('create')) render :new end end + # rubocop:enable Metrics/AbcSize # PATCH/PUT /api_clients/:id + # rubocop:disable Metrics/AbcSize def update @api_client = ApiClient.find(params[:id]) authorize(@api_client) @@ -64,22 +65,23 @@ def update attrs = remove_org_selection_params(params_in: api_client_params) if @api_client.update(attrs) - flash.now[:notice] = success_message(@api_client, _("updated")) + flash.now[:notice] = success_message(@api_client, _('updated')) else - flash.now[:alert] = failure_message(@api_client, _("update")) + flash.now[:alert] = failure_message(@api_client, _('update')) end render :edit end + # rubocop:enable Metrics/AbcSize # DELETE /api_clients/:id def destroy api_client = ApiClient.find(params[:id]) authorize(api_client) if api_client.destroy - msg = success_message(api_client, _("deleted")) + msg = success_message(api_client, _('deleted')) redirect_to super_admin_api_clients_path, notice: msg else - flash.now[:alert] = failure_message(api_client, _("delete")) + flash.now[:alert] = failure_message(api_client, _('delete')) render :edit end end @@ -110,7 +112,5 @@ def api_client_params :client_id, :client_secret, :org_id, :org_name, :org_sources, :org_crosswalk) end - end - end diff --git a/app/controllers/super_admin/notifications_controller.rb b/app/controllers/super_admin/notifications_controller.rb index fba40c042c..1079534034 100644 --- a/app/controllers/super_admin/notifications_controller.rb +++ b/app/controllers/super_admin/notifications_controller.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true module SuperAdmin - + # Controller for managing system wide notifications class NotificationsController < ApplicationController - before_action :set_notification, only: %i[show edit update destroy acknowledge] before_action :set_notifications, only: :index @@ -29,29 +28,31 @@ def edit # POST /notifications # POST /notifications.json + # rubocop:disable Metrics/AbcSize def create authorize(Notification) @notification = Notification.new(notification_params) # Will eventually need to be removed if we introduce new notification types - @notification.notification_type = "global" + @notification.notification_type = 'global' if @notification.save - flash.now[:notice] = success_message(@notification, _("created")) + flash.now[:notice] = success_message(@notification, _('created')) redirect_to edit_super_admin_notification_path(@notification) else - flash.now[:alert] = failure_message(@notification, _("create")) + flash.now[:alert] = failure_message(@notification, _('create')) render :new end end + # rubocop:enable Metrics/AbcSize # PATCH/PUT /notifications/1 # PATCH/PUT /notifications/1.json def update authorize(Notification) if @notification.update(notification_params) - flash.now[:notice] = success_message(@notification, _("updated")) + flash.now[:notice] = success_message(@notification, _('updated')) return redirect_to edit_super_admin_notification_path(@notification) else - flash.now[:alert] = failure_message(@notification, _("update")) + flash.now[:alert] = failure_message(@notification, _('update')) end render :edit end @@ -60,13 +61,13 @@ def update def enable notification = Notification.find(params[:id]) authorize(Notification) - notification.enabled = (params[:enabled] == "1") + notification.enabled = (params[:enabled] == '1') # rubocop:disable Layout/LineLength if notification.save render json: { code: 1, - msg: (notification.enabled ? _("Your notification is now active.") : _("Your notification is no longer active.")) + msg: (notification.enabled ? _('Your notification is now active.') : _('Your notification is no longer active.')) } else render status: :bad_request, json: { @@ -81,10 +82,10 @@ def enable def destroy authorize(Notification) if @notification.destroy - msg = success_message(@notification, _("deleted")) + msg = success_message(@notification, _('deleted')) redirect_to super_admin_notifications_path, notice: msg else - flash.now[:alert] = failure_message(@notification, _("delete")) + flash.now[:alert] = failure_message(@notification, _('delete')) render :edit end end @@ -101,8 +102,8 @@ def acknowledge def set_notification @notification = Notification.find(params[:id] || params[:notification_id]) rescue ActiveRecord::RecordNotFound - flash[:alert] = _("There is no notification associated with id %{id}") % - { id: params[:id] } + flash[:alert] = format(_('There is no notification associated with id %{id}'), + id: params[:id]) redirect_to action: :index end @@ -116,7 +117,5 @@ def notification_params params.require(:notification).permit(:title, :level, :body, :dismissable, :enabled, :starts_at, :expires_at) end - end - end diff --git a/app/controllers/super_admin/org_swaps_controller.rb b/app/controllers/super_admin/org_swaps_controller.rb index ff79822e0f..4918ee7589 100644 --- a/app/controllers/super_admin/org_swaps_controller.rb +++ b/app/controllers/super_admin/org_swaps_controller.rb @@ -1,39 +1,43 @@ # frozen_string_literal: true -class SuperAdmin::OrgSwapsController < ApplicationController - - include OrgSelectable - - after_action :verify_authorized - - def create - # Allows the user to swap their org affiliation on the fly - authorize current_user, :org_swap? - - # See if the user selected a new Org via the Org Lookup and - # convert it into an Org - lookup = org_from_params(params_in: org_swap_params) - - # rubocop:disable Layout/LineLength - if lookup.present? && !lookup.new_record? - current_user.org = lookup - if current_user.save - redirect_back(fallback_location: root_path, - notice: _("Your organisation affiliation has been changed. You may now edit templates for %{org_name}.") % { org_name: current_user.org.name }) +module SuperAdmin + # Controller that handles changing a Super Admin's Org affiliation on the Templates page + class OrgSwapsController < ApplicationController + include OrgSelectable + + after_action :verify_authorized + + # rubocop:disable Metrics/AbcSize + def create + # Allows the user to swap their org affiliation on the fly + authorize(current_user, :org_swap?) + + # See if the user selected a new Org via the Org Lookup and + # convert it into an Org + lookup = org_from_params(params_in: org_swap_params) + + # rubocop:disable Layout/LineLength + if lookup.present? && !lookup.new_record? + current_user.org = lookup + if current_user.save + redirect_back(fallback_location: root_path, + notice: format(_('Your organisation affiliation has been changed. You may now edit templates for %{org_name}.'), + org_name: current_user.org.name)) + else + redirect_back(fallback_location: root_path, + alert: _('Unable to change your organisation affiliation at this time.')) + end else - redirect_back(fallback_location: root_path, - alert: _("Unable to change your organisation affiliation at this time.")) + redirect_back(fallback_location: root_path, alert: _('Unknown organisation.')) end - else - redirect_back(fallback_location: root_path, alert: _("Unknown organisation.")) + # rubocop:enable Layout/LineLength end - # rubocop:enable Layout/LineLength - end + # rubocop:enable Metrics/AbcSize - private + private - def org_swap_params - params.require(:user).permit(:org_id, :org_name, :org_crosswalk) + def org_swap_params + params.require(:user).permit(:org_id, :org_name, :org_crosswalk) + end end - end diff --git a/app/controllers/super_admin/orgs_controller.rb b/app/controllers/super_admin/orgs_controller.rb index b398315e62..50ff0b421b 100644 --- a/app/controllers/super_admin/orgs_controller.rb +++ b/app/controllers/super_admin/orgs_controller.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true module SuperAdmin - + # Controller for creating and deleting Orgs class OrgsController < ApplicationController - include OrgSelectable after_action :verify_authorized @@ -11,7 +10,7 @@ class OrgsController < ApplicationController # GET /super_admin/orgs def index authorize Org - render "index", locals: { + render 'index', locals: { orgs: Org.includes(:contributors, :plans).with_template_and_user_counts.page(1) } end @@ -20,11 +19,11 @@ def index def new @org = Org.new(managed: true) authorize @org - @org.links = { "org": [] } + @org.links = { org: [] } end # POST /super_admin/orgs - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity def create authorize Org attrs = org_params @@ -41,7 +40,7 @@ def create org = Org.new unless org.present? org.language = Language.default - org.managed = org_params[:managed] == "1" + org.managed = org_params[:managed] == '1' org.logo = params[:logo] if params[:logo] org.links = if params[:org_links].present? JSON.parse(params[:org_links]) @@ -58,34 +57,30 @@ def create org.institution = params[:institution].present? org.organisation = params[:organisation].present? - # TODO: Add view for changing granularity for API permissions if needed - # For now follow small workaround to give access to full API for org - # Admins for new oganizations - org.token_permission_types = TokenPermissionType.all - if org.update(attrs) - msg = success_message(org, _("created")) + msg = success_message(org, _('created')) redirect_to admin_edit_org_path(org.id), notice: msg else - flash.now[:alert] = failure_message(org, _("create")) + flash.now[:alert] = failure_message(org, _('create')) @org = org - @org.links = { "org": [] } unless org.links.present? - render "super_admin/orgs/new" + @org.links = { org: [] } unless org.links.present? + render 'super_admin/orgs/new' end rescue Dragonfly::Job::Fetch::NotFound - failure = _("There seems to be a problem with your logo. Please upload it again.") + failure = _('There seems to be a problem with your logo. Please upload it again.') redirect_to admin_edit_org_path(org), alert: failure - render "orgs/admin_edit", locals: { + render 'orgs/admin_edit', locals: { org: org, - languages: Language.all.order("name"), - method: "POST", + languages: Language.all.order('name'), + method: 'POST', url: super_admin_orgs_path } end end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity # DELETE /super_admin/orgs/:id + # rubocop:disable Metrics/AbcSize def destroy org = Org.includes(:users, :templates, :guidance_groups).find(params[:id]) authorize org @@ -96,13 +91,14 @@ def destroy org.guidance_groups.delete_all if org.destroy! - msg = success_message(org, _("removed")) + msg = success_message(org, _('removed')) redirect_to super_admin_orgs_path, notice: msg else - failure = failure_message(org, _("remove")) + failure = failure_message(org, _('remove')) redirect_to super_admin_orgs_path, alert: failure end end + # rubocop:enable Metrics/AbcSize # POST /super_admin/:id/merge_analyze def merge_analyze @@ -126,6 +122,7 @@ def merge_analyze end # POST /super_admin/:id/merge_commit + # rubocop:disable Metrics/AbcSize def merge_commit @org = Org.find(params[:id]) authorize @org @@ -137,17 +134,18 @@ def merge_commit msg = "Successfully merged '#{@org.name}' into '#{@target_org.name}'" redirect_to super_admin_orgs_path, notice: msg else - msg = _("An error occurred while trying to merge the Organisations.") + msg = _('An error occurred while trying to merge the Organisations.') redirect_to admin_edit_org_path(@org), alert: msg end else - msg = _("Unable to merge the two Organisations at this time.") + msg = _('Unable to merge the two Organisations at this time.') redirect_to admin_edit_org_path(@org), alert: msg end rescue JSON::ParserError - msg = _("Unable to determine what records need to be merged.") + msg = _('Unable to determine what records need to be merged.') redirect_to admin_edit_org_path(@org), alert: msg end + # rubocop:enable Metrics/AbcSize private @@ -155,13 +153,12 @@ def org_params params.require(:org).permit(:name, :abbreviation, :logo, :managed, :contact_email, :contact_name, :remove_logo, :feedback_enabled, :feedback_msg, - :org_id, :org_name, :org_crosswalk) + :org_id, :org_name, :org_crosswalk, + :funder, :institution, :organisation) end def merge_params params.require(:org).permit(:org_name, :org_sources, :org_crosswalk, :id, :target_org) end - end - end diff --git a/app/controllers/super_admin/themes_controller.rb b/app/controllers/super_admin/themes_controller.rb index 5738acc10a..6be6852c59 100644 --- a/app/controllers/super_admin/themes_controller.rb +++ b/app/controllers/super_admin/themes_controller.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true module SuperAdmin - + # Controller for managing Themes class ThemesController < ApplicationController - helper PaginableHelper def index authorize(Theme) @@ -19,10 +18,10 @@ def create authorize(Theme) @theme = Theme.new(permitted_params) if @theme.save - flash.now[:notice] = success_message(@theme, _("created")) + flash.now[:notice] = success_message(@theme, _('created')) render :edit else - flash.now[:alert] = failure_message(@theme, _("create")) + flash.now[:alert] = failure_message(@theme, _('create')) render :new end end @@ -32,36 +31,36 @@ def edit @theme = Theme.find(params[:id]) end + # rubocop:disable Metrics/AbcSize def update authorize(Theme) @theme = Theme.find(params[:id]) if @theme.update_attributes(permitted_params) - flash.now[:notice] = success_message(@theme, _("updated")) + flash.now[:notice] = success_message(@theme, _('updated')) else - flash.now[:alert] = failure_message(@theme, _("update")) + flash.now[:alert] = failure_message(@theme, _('update')) end render :edit end + # rubocop:enable Metrics/AbcSize def destroy authorize(Theme) @theme = Theme.find(params[:id]) if @theme.destroy - msg = success_message(@theme, _("deleted")) + msg = success_message(@theme, _('deleted')) redirect_to super_admin_themes_path, notice: msg else - flash.now[:alert] = failure_message(@theme, _("delete")) + flash.now[:alert] = failure_message(@theme, _('delete')) render :edit end end - # Private instance methods + private def permitted_params params.require(:theme).permit(:title, :description) end - end - end diff --git a/app/controllers/super_admin/users_controller.rb b/app/controllers/super_admin/users_controller.rb index 933ac6e5c5..4c67446d60 100644 --- a/app/controllers/super_admin/users_controller.rb +++ b/app/controllers/super_admin/users_controller.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true module SuperAdmin - + # Controller for performing CRUD operations for other users class UsersController < ApplicationController - include OrgSelectable after_action :verify_authorized @@ -14,7 +13,7 @@ def edit authorize @user @departments = @user.org.departments.order(:name) @plans = Plan.active(@user).page(1) - render "super_admin/users/edit", + render 'super_admin/users/edit', locals: { user: @user, departments: @departments, plans: @plans, @@ -25,7 +24,7 @@ def edit end # PUT /super_admin/users/:id - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def update @user = User.find(params[:id]) authorize @user @@ -52,24 +51,25 @@ def update end @user.update(org_id: lookup.id) if lookup.present? - flash.now[:notice] = success_message(@user, _("updated")) + flash.now[:notice] = success_message(@user, _('updated')) else - flash.now[:alert] = failure_message(@user, _("update")) + flash.now[:alert] = failure_message(@user, _('update')) end render :edit end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength # PUT /super_admin/users/:id/merge + # rubocop:disable Metrics/AbcSize def merge @user = User.find(params[:id]) authorize @user - if params[:id] != params[:merge_id] - merge_accounts - else + if params[:id] == params[:merge_id] flash.now[:alert] = _("You attempted to merge 2 accounts with the same email address. Please merge with a different email address.") + else + merge_accounts end # After merge attempt get departments and plans @@ -78,38 +78,43 @@ def merge render :edit end + # rubocop:enable Metrics/AbcSize # GET /super_admin/users/:id/search + # rubocop:disable Metrics/AbcSize def search @user = User.find(params[:id]) - @users = User.where("email LIKE ?", "%#{params[:email]}%") + @users = User.where('email LIKE ?', "%#{params[:email]}%") authorize @users @departments = @user.org.departments.order(:name) @plans = Plan.active(@user).page(1) # WHAT TO RETURN!?!?! if @users.present? # found a user, or Users, submit for merge render json: { - form: render_to_string(partial: "super_admin/users/confirm_merge.html.erb") + form: render_to_string(partial: 'super_admin/users/confirm_merge.html.erb') } else # NO USER, re-render w/error? - flash.now[:alert] = "Unable to find user" + flash.now[:alert] = 'Unable to find user' render :edit # re-do as responding w/ json end end + # rubocop:enable Metrics/AbcSize # PUT /super_admin/users/:id/archive + # rubocop:disable Metrics/AbcSize def archive @user = User.find(params[:id]) authorize @user @departments = @user.org.departments.order(:name) @plans = Plan.active(@user).page(1) if @user.archive - flash.now[:notice] = success_message(@user, _("archived")) + flash.now[:notice] = success_message(@user, _('archived')) else - flash.now[:alert] = failure_message(@user, _("archive")) + flash.now[:alert] = failure_message(@user, _('archive')) end render :edit end + # rubocop:enable Metrics/AbcSize private @@ -126,12 +131,10 @@ def user_params def merge_accounts remove = User.find(params[:merge_id]) if @user.merge(remove) - flash.now[:notice] = success_message(@user, _("merged")) + flash.now[:notice] = success_message(@user, _('merged')) else - flash.now[:alert] = failure_message(@user, _("merge")) + flash.now[:alert] = failure_message(@user, _('merge')) end end - end - end diff --git a/app/controllers/template_options_controller.rb b/app/controllers/template_options_controller.rb index 48b1511320..61d6f42017 100644 --- a/app/controllers/template_options_controller.rb +++ b/app/controllers/template_options_controller.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true +# Controller that determines which templates are displayed/selected for the user when +# they are creating a new plan class TemplateOptionsController < ApplicationController - include OrgSelectable after_action :verify_authorized @@ -9,6 +10,7 @@ class TemplateOptionsController < ApplicationController # GET /template_options (AJAX) # Collect all of the templates available for the org+funder combination # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def index org_hash = plan_params.fetch(:research_org_id, {}) @@ -20,53 +22,53 @@ def index @templates = [] - if (org.present? && !org.new_record?) || (funder.present? && !funder.new_record?) - if funder.present? && !funder.new_record? - if org.present? && !org.new_record? - # Load the funder's template(s) minus the default template (that gets swapped - # in below if NO other templates are available) - @templates = Template.latest_customizable.where(org_id: funder.id, is_default: false).to_a - # Swap out any organisational cusotmizations of a funder template - @templates = @templates.map do |tmplt| - customization = Template.published - .latest_customized_version(tmplt.family_id, - org.id).first - # Only provide the customized version if its still up to date with the - # funder template! - # rubocop:disable Metrics/BlockNesting - if customization.present? && !customization.upgrade_customization? - customization - else - tmplt - end - end - # We are using a default funder to provide with the default templates, but - # We still want to provide the organization templates. - # If the no funder was specified OR the funder matches the org - # if funder.blank? || funder.id == org&.id - # Retrieve the Org's templates - @templates << Template.published.organisationally_visible.where(org_id: org.id, customization_of: nil).to_a - @templates = @templates.flatten.uniq - else # if'No Primary Research Institution' checkbox is checked, only show publicly available template without customization - @templates = Template.published.publicly_visible.where(org_id: funder.id, customization_of: nil) - end - # DMP Assistant: We do not want to include not customized templates from default funder - # Include customizable funder templates - # @templates << funder_templates = Template.latest_customizable - # Always use the default template - if Template.default.present? && org.present? - customization = Template.published.latest_customized_version(Template.default.family_id, org.id).first - customization = Template.default unless customization - @templates.select! { |t| t.id != Template.default.id && t.id != customization.id} - # We want the default template to appear at the beggining of the list - @templates.unshift(customization) + return unless (org.present? && !org.new_record?) || (funder.present? && !funder.new_record?) + return unless funder.present? && !funder.new_record? + + if org.present? && !org.new_record? + # Load the funder's template(s) minus the default template (that gets swapped + # in below if NO other templates are available) + @templates = Template.latest_customizable.where(org_id: funder.id, is_default: false).to_a + # Swap out any organisational cusotmizations of a funder template + @templates = @templates.map do |tmplt| + customization = Template.published + .latest_customized_version(tmplt.family_id, + org.id).first + # Only provide the customized version if its still up to date with the + # funder template! + if customization.present? && !customization.upgrade_customization? + customization + else + tmplt end - @templates = @templates.uniq.sort_by(&:title) end + # We are using a default funder to provide with the default templates, but + # We still want to provide the organization templates. + # If the no funder was specified OR the funder matches the org + # if funder.blank? || funder.id == org&.id + # Retrieve the Org's templates + @templates << Template.published.organisationally_visible.where(org_id: org.id, customization_of: nil).to_a + @templates = @templates.flatten.uniq + else + # if'No Primary Research Institution' checkbox is checked, + # only show publicly available template without customization + @templates = Template.published.publicly_visible.where(org_id: funder.id, customization_of: nil) end + # DMP Assistant: We do not want to include not customized templates from default funder + # Include customizable funder templates + # @templates << funder_templates = Template.latest_customizable + # Always use the default template + if Template.default.present? && org.present? + customization = Template.published.latest_customized_version(Template.default.family_id, org.id).first + customization ||= Template.default + @templates.select! { |t| t.id != Template.default.id && t.id != customization.id } + # We want the default template to appear at the beggining of the list + @templates.unshift(customization) + end + @templates = @templates.uniq.sort_by(&:title) end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity private @@ -78,5 +80,4 @@ def plan_params def org_params %i[id name url language abbreviation ror fundref weight score] end - end diff --git a/app/controllers/usage_controller.rb b/app/controllers/usage_controller.rb index d2b756a95e..78e5e93264 100644 --- a/app/controllers/usage_controller.rb +++ b/app/controllers/usage_controller.rb @@ -1,14 +1,15 @@ # frozen_string_literal: true -class UsageController < ApplicationController +# Integration: based on ISSU68 use created_plan instead of complected_plans - require "csvable" +# Controller for site usage statistics +class UsageController < ApplicationController + require 'csvable' after_action :verify_authorized # GET /usage def index authorize :usage - args = default_query_args user_data(args: args, as_json: true) plan_data(args: args, as_json: true) @@ -26,9 +27,8 @@ def plans_by_template authorize :usage args = default_query_args - if usage_params["template_plans_range"].present? - args[:start_date] = usage_params["template_plans_range"] - end + # args[:start_date] = usage_params['template_plans_range'] if usage_params['template_plans_range'].present? + args[:start_date] = usage_params['template_plans_range'] if usage_params['template_plans_range'].present? plan_data(args: args, as_json: true) end @@ -42,7 +42,7 @@ def global_statistics sep = sep_param data_csvified = Csvable.from_array_of_hashes(data, true, sep) - send_data(data_csvified, filename: "totals.csv") + send_data(data_csvified, filename: 'totals.csv') end # GET @@ -53,22 +53,21 @@ def org_statistics sep = sep_param data_csvified = Csvable.from_array_of_hashes(data, true, sep) - send_data(data_csvified, filename: "totals.csv") + send_data(data_csvified, filename: 'totals.csv') end - # POST /usage_filter + # POST /usage_filter # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/AbcSize def filter # This action is triggered when a user specifies a date range. # A super admin can pass along a nil organization to fetch compounded # statistics authorize :usage - args = args_from_params @topic = usage_params[:topic] - case @topic - when "plans" + when 'plans' plan_data(args: args) total_plans(args: min_max_dates(args: args)) @total = @total_org_plans @@ -97,17 +96,19 @@ def yearly_users user_data(args: default_query_args) sep = sep_param send_data(CSV.generate(col_sep: sep) do |csv| - csv << [_("Month"), _("No. Users joined")] + csv << [_('Month'), _('No. Users joined')] total = 0 @users_per_month.each do |data| - csv << [data.date.strftime("%b-%y"), data.count] + csv << [data.date.strftime('%b-%y'), data.count] total += data.count end - csv << [_("Total"), total] - end, filename: "users_joined.csv") + csv << [_('Total'), total] + end, filename: 'users_joined.csv') end + # rubocop:enable Metrics/AbcSize # GET /usage_yearly_plans + # rubocop:disable Metrics/AbcSize def yearly_plans # This action is triggered when a user clicks on the 'download csv' button # for the annual plans chart @@ -116,15 +117,16 @@ def yearly_plans plan_data(args: default_query_args) sep = sep_param send_data(CSV.generate(col_sep: sep) do |csv| - csv << [_("Month"), _("No. Created Plans")] + csv << [_('Month'), _('No. Created Plans')] total = 0 @plans_per_month.each do |data| - csv << [data.date.strftime("%b-%y"), data.count] + csv << [data.date.strftime('%b-%y'), data.count] total += data.count end - csv << [_("Total"), total] - end, filename: "created_plans.csv") + csv << [_('Total'), total] + end, filename: 'created_plans.csv') end + # rubocop:enable Metrics/AbcSize # GET /usage_all_plans_by_template def all_plans_by_template @@ -137,10 +139,8 @@ def all_plans_by_template sep = sep_param plan_data(args: args, sort: :desc) - # rubocop:disable Layout/LineLength data_csvified = StatCreatedPlan.to_csv(@plans_per_month, details: { by_template: true, sep: sep }) - # rubocop:enable Layout/LineLength - send_data(data_csvified, filename: "created_plan_by_template.csv") + send_data(data_csvified, filename: 'completed_plan_by_template.csv') end private @@ -164,8 +164,8 @@ def args_from_params { org: org, - start_date: start_date.present? ? start_date : first_user_date.strftime("%Y-%m-%d"), - end_date: end_date.present? ? end_date : Date.today.strftime("%Y-%m-%d") + start_date: start_date.present? ? start_date : first_user_date.strftime('%Y-%m-%d'), + end_date: end_date.present? ? end_date : Date.today.strftime('%Y-%m-%d') } end # rubocop:enable Metrics/AbcSize @@ -177,24 +177,24 @@ def default_query_args # That means we want our date range to be 11/30/2018 to 11/30/2019 { org: current_user.org, - start_date: Date.today.months_ago(12).end_of_month.strftime("%Y-%m-%d"), - end_date: Date.today.last_month.end_of_month.strftime("%Y-%m-%d"), + start_date: Date.today.months_ago(12).end_of_month.strftime('%Y-%m-%d'), + end_date: Date.today.last_month.end_of_month.strftime('%Y-%m-%d'), filtered: parse_filtered } end def parse_filtered - params[:filtered].present? && params[:filtered] == "true" + params[:filtered].present? && params[:filtered] == 'true' end # set the csv separator or default to comma def sep_param - params["sep"] || "," + params['sep'] || ',' end def min_max_dates(args:) - args[:start_date] = first_plan_date.strftime("%Y-%m-%d") - args[:end_date] = Date.today.strftime("%Y-%m-%d") + args[:start_date] = first_plan_date.strftime('%Y-%m-%d') + args[:end_date] = Date.today.strftime('%Y-%m-%d') args end @@ -206,7 +206,7 @@ def user_data(args:, as_json: false, sort: :asc) def plan_data(args:, as_json: false, sort: :asc) @plans_per_month = StatCreatedPlan.monthly_range(args) - .where.not(details: "{\"by_template\":[]}") + .where.not(details: '{"by_template":[]}') .order(date: sort) @plans_per_month = @plans_per_month.map(&:to_json) if as_json end @@ -215,12 +215,12 @@ def total_plans(args:) @total_org_plans = StatCreatedPlan.monthly_range(args).sum(:count) end - def total_users(args:) - @total_org_users = StatJoinedUser.monthly_range(args.except(:filtered)).sum(:count) + def total_organizations(*) + @total_organizations = Org.count end - def total_organizations(args:) - @total_organizations = Org.count + def total_users(args:) + @total_org_users = StatJoinedUser.monthly_range(args.except(:filtered)).sum(:count) end def ranged_organizations(args:) @@ -229,8 +229,7 @@ def ranged_organizations(args:) # continue to use start_date = DateTime.parse(args[:start_date]) end_date = DateTime.parse(args[:end_date]) - - Org.managed.where(:created_at => start_date.beginning_of_day..end_date.end_of_day) + Org.managed.where(created_at: start_date.beginning_of_day..end_date.end_of_day) end def first_plan_date @@ -241,5 +240,4 @@ def first_plan_date def first_user_date User.order(created_at: :asc).first.created_at.beginning_of_day end - end diff --git a/app/controllers/usage_downloads_controller.rb b/app/controllers/usage_downloads_controller.rb index c297b2903b..0b2b0867e9 100644 --- a/app/controllers/usage_downloads_controller.rb +++ b/app/controllers/usage_downloads_controller.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true +# Controller for generating CSV download of usage stats class UsageDownloadsController < ApplicationController - def index check_authorized! data = Org::TotalCountStatService.call data_csvified = Csvable.from_array_of_hashes(data) - send_data(data_csvified, filename: "totals.csv") + send_data(data_csvified, filename: 'totals.csv') end private @@ -18,5 +18,4 @@ def check_authorized! raise Pundit::NotAuthorizedError end end - end diff --git a/app/controllers/users/invitations_controller.rb b/app/controllers/users/invitations_controller.rb index 4b03586e1f..0505adc2fe 100644 --- a/app/controllers/users/invitations_controller.rb +++ b/app/controllers/users/invitations_controller.rb @@ -1,64 +1,65 @@ # frozen_string_literal: true -class Users::InvitationsController < Devise::InvitationsController +module Users + # Controller that handles user invitations + class InvitationsController < Devise::InvitationsController + include OrgSelectable - include OrgSelectable + # Creates the selected Org if necessary and then attaches the invited user + # to the Org after Devise does its thing + prepend_after_action :handle_org, only: [:update] + prepend_before_action :fix_org_params, only: [:update] - # Creates the selected Org if necessary and then attaches the invited user - # to the Org after Devise does its thing - prepend_after_action :handle_org, only: [:update] - prepend_before_action :fix_org_params, only: [:update] + protected - protected - - def fix_org_params - hash = org_hash_from_params(params_in: params[:user]) - org = OrgSelection::HashToOrgService.to_org(hash: hash, - allow_create: false) - params[:user][:org_id] = org&.id - end + def fix_org_params + hash = org_hash_from_params(params_in: params[:user]) + org = OrgSelection::HashToOrgService.to_org(hash: hash, + allow_create: false) + params[:user][:org_id] = org&.id + end - # Override require_no_authentication method defined at DeviseController - # (parent of Devise::InvitationsController) The following filter gets - # executed any time GET /users/invitation/accept?invitation_token=valid_token - # is requested. It replaces the default error message from devise - # (e.g. You are already signed in.) if the user is signed in already while - # trying to access to that URL - def require_no_authentication - super - return unless flash[:alert].present? + # Override require_no_authentication method defined at DeviseController + # (parent of Devise::InvitationsController) The following filter gets + # executed any time GET /users/invitation/accept?invitation_token=valid_token + # is requested. It replaces the default error message from devise + # (e.g. You are already signed in.) if the user is signed in already while + # trying to access to that URL + def require_no_authentication + super + return unless flash[:alert].present? - flash[:alert] = nil - # rubocop:disable Layout/LineLength - flash[:notice] = _("You are already signed in as another user. Please log out to activate your invitation.") - # rubocop:enable Layout/LineLength - end + flash[:alert] = nil + flash[:notice] = _('You are already signed in as another user. Please log out to activate your invitation.') + end - # Handle the user's Org selection - def handle_org - attrs = update_resource_params + # Handle the user's Org selection + # rubocop:disable Metrics/AbcSize + def handle_org + attrs = update_resource_params - return unless attrs[:org_id].present? + return unless attrs[:org_id].present? - # See if the user selected a new Org via the Org Lookup and - # convert it into an Org - lookup = org_from_params(params_in: attrs) - return nil unless lookup.present? + # See if the user selected a new Org via the Org Lookup and + # convert it into an Org + lookup = org_from_params(params_in: attrs) + return nil unless lookup.present? - # If this is a new Org we need to save it first before attaching - # it to the user - if lookup.new_record? - lookup.save - identifiers_from_params(params_in: attrs).each do |identifier| - next unless identifier.value.present? + # If this is a new Org we need to save it first before attaching + # it to the user + if lookup.new_record? + lookup.save + identifiers_from_params(params_in: attrs).each do |identifier| + next unless identifier.value.present? - identifier.identifiable = lookup - identifier.save + identifier.identifiable = lookup + identifier.save + end + lookup.reload end - lookup.reload - end - resource.update(org_id: lookup.id) + resource.update(org_id: lookup.id) + end + # rubocop:enable Metrics/AbcSize end - end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index aa7481c704..01ec5491e7 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -1,96 +1,86 @@ # frozen_string_literal: true -class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController - - ## - # Dynamically build a handler for each omniauth provider - # ------------------------------------------------------------- - IdentifierScheme.for_authentication.each do |scheme| - define_method(scheme.name.downcase) do - handle_omniauth(scheme) +module Users + # Controller that handles callbacks from OmniAuth integrations (e.g. Shibboleth and ORCID) + class OmniauthCallbacksController < Devise::OmniauthCallbacksController + ## + # Dynamically build a handler for each omniauth provider + # ------------------------------------------------------------- + IdentifierScheme.for_authentication.each do |scheme| + define_method(scheme.name.downcase) do + handle_omniauth(scheme) + end end - end - - # Processes callbacks from an omniauth provider and directs the user to - # the appropriate page: - # Not logged in and uid had no match ---> Sign Up page - # Not logged in and uid had a match ---> Sign In and go to Home Page - # Signed in and uid had no match --> Save the uid and go to the Profile Page - # Signed in and uid had a match --> Go to the Home Page - # - # scheme - The IdentifierScheme for the provider - # - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - def handle_omniauth(scheme) - user = if request.env["omniauth.auth"].nil? - User.from_omniauth(request.env) - else - User.from_omniauth(request.env["omniauth.auth"]) - end - # If the user isn't logged in - if current_user.nil? - # If the uid didn't have a match in the system send them to register - if user.nil? - session["devise.#{scheme.name.downcase}_data"] = request.env["omniauth.auth"] - redirect_to new_user_registration_url - - # Otherwise sign them in - elsif scheme.name == "shibboleth" - # Until ORCID becomes supported as a login method - set_flash_message(:notice, :success, kind: scheme.description) if is_navigational_format? - sign_in_and_redirect user, event: :authentication - else - flash[:notice] = _("Successfully signed in") - redirect_to new_user_registration_url - end + # Processes callbacks from an omniauth provider and directs the user to + # the appropriate page: + # Not logged in and uid had no match ---> Sign Up page + # Not logged in and uid had a match ---> Sign In and go to Home Page + # Signed in and uid had no match --> Save the uid and go to the Profile Page + # Signed in and uid had a match --> Go to the Home Page + # + # scheme - The IdentifierScheme for the provider + # + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def handle_omniauth(scheme) + user = if request.env['omniauth.auth'].nil? + User.from_omniauth(request.env) + else + User.from_omniauth(request.env['omniauth.auth']) + end - # The user is already logged in and just registering the uid with us - else - # If the user could not be found by that uid then attach it to their record - if user.nil? - if Identifier.create(identifier_scheme: scheme, - value: request.env["omniauth.auth"].uid, - attrs: request.env["omniauth.auth"], - identifiable: current_user) - flash[:notice] = _("Your account has been successfully linked to %{scheme}.") % { - scheme: scheme.description - } + # If the user isn't logged in + if current_user.nil? + # If the uid didn't have a match in the system send them to register + if user.nil? + session["devise.#{scheme.name.downcase}_data"] = request.env['omniauth.auth'] + redirect_to new_user_registration_url + # Otherwise sign them in + elsif scheme.name == 'shibboleth' + # Until ORCID becomes supported as a login method + set_flash_message(:notice, :success, kind: scheme.description) if is_navigational_format? + sign_in_and_redirect user, event: :authentication else - flash[:alert] = _("Unable to link your account to %{scheme}.") % { - scheme: scheme.description - } + flash[:notice] = _('Successfully signed in') + redirect_to new_user_registration_url end - elsif user.id != current_user.id - # If a user was found but does NOT match the current user then the identifier has - # already been attached to another account (likely the user has 2 accounts) - identifier = UserIdentifier.where( - identifier: request.env["omniauth.auth"].uid - ).first - if identifier.user.id != current_user.id - # rubocop:disable Metrics/LineLength - flash[:alert] = _("The current %{scheme_description} iD has been already linked to a user with email %{user_email}") % { - scheme_description: scheme.description, - user_email: identifier.user.email - } - # rubocop:enable Metrics/LineLength + # The user is already logged in and just registering the uid with us + else + # If the user could not be found by that uid then attach it to their record + if user.nil? + if Identifier.create(identifier_scheme: scheme, + value: request.env['omniauth.auth'].uid, + attrs: request.env['omniauth.auth'], + identifiable: current_user) + flash[:notice] = + format(_('Your account has been successfully linked to %{scheme}.'), + scheme: scheme.description) + + else + flash[:alert] = format(_('Unable to link your account to %{scheme}.'), + scheme: scheme.description) + end + + elsif user.id != current_user.id + # If a user was found but does NOT match the current user then the identifier has + # already been attached to another account (likely the user has 2 accounts) + # rubocop:disable Layout/LineLength + flash[:alert] = _("The current #{scheme.description} iD has been already linked to a user with email #{identifier.user.email}") + # rubocop:enable Layout/LineLength end - # Otherwise, the identifier was found and it matches the one already associated - # with the current user so nothing else needs to be done + # Redirect to the User Profile page + redirect_to edit_user_registration_path end - - # Redirect to the User Profile page - redirect_to edit_user_registration_path end - end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity - def failure - redirect_to root_path + def failure + redirect_to root_path + end end - end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 37a48fd3ba..6b0269f487 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Controller that handles Admin operations for managing users class UsersController < ApplicationController - helper PaginableHelper helper PermsHelper include ConditionalUserMailer @@ -11,6 +11,7 @@ class UsersController < ApplicationController ## # GET - List of all users for an organisation # Displays number of roles[was project_group], name, email, and last sign in + # rubocop:disable Metrics/AbcSize def admin_index authorize User @@ -32,6 +33,7 @@ def admin_index end end end + # rubocop:enable Metrics/AbcSize ## # GET - Displays the permissions available to the selected user @@ -50,9 +52,9 @@ def admin_grant_permissions end render json: { - "user" => { - "id" => user.id, - "html" => render_to_string(partial: "users/admin_grant_permissions", + 'user' => { + 'id' => user.id, + 'html' => render_to_string(partial: 'users/admin_grant_permissions', locals: { user: user, perms: perms }, formats: [:html]) } @@ -64,6 +66,7 @@ def admin_grant_permissions # redirects to the admin_index action # should add validation that the perms given are current perms of the current_user # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def admin_update_permissions @user = User.find(params[:id]) authorize @user @@ -89,24 +92,25 @@ def admin_update_permissions if @user.save if privileges_changed - deliver_if(recipients: @user, key: "users.admin_privileges") do |r| + deliver_if(recipients: @user, key: 'users.admin_privileges') do |r| UserMailer.admin_privileges(r).deliver_now end end render(json: { code: 1, - msg: success_message(perms.first_or_initialize, _("saved")), - current_privileges: render_to_string(partial: "users/current_privileges", + msg: success_message(perms.first_or_initialize, _('saved')), + current_privileges: render_to_string(partial: 'users/current_privileges', locals: { user: @user }, formats: [:html]) }) else - render(json: { code: 0, msg: failure_message(@user, _("updated")) }) + render(json: { code: 0, msg: failure_message(@user, _('updated')) }) end end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # PUT /users/:id/update_email_preferences + # rubocop:disable Metrics/AbcSize def update_email_preferences prefs = preference_params authorize User @@ -117,16 +121,18 @@ def update_email_preferences pref.settings = {} pref.user = current_user end - pref.settings["email"] = booleanize_hash(prefs["prefs"]) + pref.settings['email'] = booleanize_hash(prefs['prefs']) pref.save # Include active tab in redirect path - redirect_to "#{edit_user_registration_path}\#notification-preferences", - notice: success_message(pref, _("saved")) + redirect_to "#{edit_user_registration_path}#notification-preferences", + notice: success_message(pref, _('saved')) end + # rubocop:enable Metrics/AbcSize # PUT /users/:id/activate # ----------------------------------------------------- + # rubocop:disable Metrics/AbcSize def activate authorize current_user @@ -138,21 +144,20 @@ def activate user.save! render json: { code: 1, - msg: _("Successfully %{action} %{username}'s account.") % { - action: user.active ? _("activated") : _("deactivated"), - username: user.name(false) - } + msg: format(_("Successfully %{action} %{username}'s account."), + action: user.active ? _('activated') : _('deactivated'), + username: user.name(false)) } rescue StandardError render json: { code: 0, - msg: _("Unable to %{action} %{username}") % { - action: user.active ? _("activate") : _("deactivate"), - username: user.name(false) - } + msg: format(_('Unable to %{action} %{username}'), + action: user.active ? _('activate') : _('deactivate'), + username: user.name(false)) } end end + # rubocop:enable Metrics/AbcSize # POST /users/acknowledge_notification def acknowledge_notification @@ -198,7 +203,7 @@ def preference_params def booleanize_hash(node) # leaf: convert to boolean and return # hash: iterate over leaves - return node == "true" unless node.is_a?(ActionController::Parameters) + return node == 'true' unless node.is_a?(ActionController::Parameters) newnode = {} node.each do |key, value| @@ -206,5 +211,4 @@ def booleanize_hash(node) end newnode end - end diff --git a/app/helpers/annotations_helper.rb b/app/helpers/annotations_helper.rb index 16b1c10729..3104bf0a01 100644 --- a/app/helpers/annotations_helper.rb +++ b/app/helpers/annotations_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Helper methods for Annotations module AnnotationsHelper - # rubocop:disable all TOOLTIPS_FOR_TEXT = { example_answer: _("You can add an example answer to help users respond. These will be presented above the answer box and can be copied/ pasted."), @@ -12,5 +12,4 @@ module AnnotationsHelper def tooltip_for_annotation_text(annotation) TOOLTIPS_FOR_TEXT[annotation.type.to_sym] end - end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0cfb5e78a6..8113321562 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Generic helper methods module ApplicationHelper - def resource_name :user end @@ -20,6 +20,7 @@ def devise_mapping # params) of the last URL requested. See # http://api.rubyonrails.org/classes/ActionDispatch/Request.html#method-i-fullpath # for details + # rubocop:disable Style/OptionalBooleanParameter def active_page?(path, exact_match = false) if exact_match request.fullpath == path @@ -27,6 +28,7 @@ def active_page?(path, exact_match = false) request.fullpath.include?(path) end end + # rubocop:enable Style/OptionalBooleanParameter alias isActivePage active_page? @@ -40,12 +42,11 @@ def title(page_title) content_for(:title) { page_title } end - # This method assumes there will be an image file called dmp_logo_xx_XX.png # Where xx_XX is the current locale in ww-WW format. Examples of this are # en_CA, fr_CA def current_locale_logo - file_name = if FeatureFlagHelper.enabled?(:on_sandbox) + if FeatureFlagHelper.enabled?(:on_sandbox) "sandbox_logo_#{I18n.locale}.png" else "dmp_logo_#{I18n.locale}.png" @@ -54,40 +55,40 @@ def current_locale_logo # We are overriding this method in order to provide different contact us urls # based on the chosen locale. Using the branding.yml does not work for this as - # we need different urls. This will be changed when we move to DMPRoadmap 3.0 + # we need different urls. This will be changed when we move to DMPRoadmap 3.0 # as there is a service that handles fetching this information. def contact_us_path - if (I18n.locale == 'fr_CA') + if I18n.locale == 'fr_CA' 'https://portagenetwork.ca/fr/contactez-nous/' else - # Handling 'en_CA' locale + # Handling "en_CA" locale 'https://portagenetwork.ca/contact-us/' end end def terms_of_use_path - if (I18n.locale == 'fr_CA') + if I18n.locale == 'fr_CA' 'https://portagenetwork.ca/fr/outils-et-ressources/assistant-pgd/conditions-dutilisation-de-lassistant-pgd/' else - # Handling 'en_CA' locale + # Handling "en_CA" locale 'https://portagenetwork.ca/tools-and-resources/dmp-assistant/dmp-assistant-terms-of-use/' end end def how_to_manage_your_data_path - if (I18n.locale == 'fr_CA') + if I18n.locale == 'fr_CA' 'https://portagenetwork.ca/fr/outils-et-ressources/assistant-pgd/comment-gerer-vos-donnees/' else - # Handling 'en_CA' locale + # 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') + if I18n.locale == 'fr_CA' 'https://portagenetwork.ca/fr/outils-et-ressources/personnes-ressources-pour-la-gdr-dans-les-etablissements/' else - # Handling 'en_CA' locale + # Handling "en_CA" locale 'https://portagenetwork.ca/tools-and-resources/institutional-rdm-contacts/' end end @@ -97,5 +98,4 @@ def unique_dom_id(record, prefix = nil) record_id = record_key_for_dom_id(record) || record.object_id "#{klass}_#{record_id}" end - end diff --git a/app/helpers/conditions_helper.rb b/app/helpers/conditions_helper.rb index a674017fdc..8b6a956c14 100644 --- a/app/helpers/conditions_helper.rb +++ b/app/helpers/conditions_helper.rb @@ -2,28 +2,23 @@ DISPLAY_LENGTH = 50 +# Helper methods for Conditional Questions # rubocop:disable Metrics/ModuleLength module ConditionsHelper - # return a list of question ids to open/hide def remove_list(object) id_list = [] - if object.is_a?(Plan) - plan_answers = object.answers - elsif object.is_a?(Hash) - plan_answers = object[:answers] - else - # TODO: change this to an exception as it shouldn't happen - return [] - end - plan_answers.each do |answer| - id_list += answer_remove_list(answer) - end + plan_answers = object.answers if object.is_a?(Plan) + plan_answers = object[:answers] if object.is_a?(Hash) + return [] unless plan_answers.present? + + plan_answers.each { |answer| id_list += answer_remove_list(answer) } id_list end # returns an array of ids to remove based on the conditions associated with an answer # or trigger the email (TODO: combining these is a bit icky!) + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def answer_remove_list(answer, user = nil) id_list = [] return id_list unless answer.question.option_based? @@ -33,23 +28,25 @@ def answer_remove_list(answer, user = nil) action = cond.action_type chosen = answer.question_option_ids.sort if chosen == opts - if action == "remove" + if action == 'remove' rems = cond.remove_data.map(&:to_i) id_list += rems elsif !user.nil? UserMailer.question_answered(JSON.parse(cond.webhook_data), user, answer, - chosen.join(" and ")).deliver_now + chosen.join(' and ')).deliver_now end end end # uniq because could get same remove id from diff conds id_list.uniq end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def send_webhooks(user, answer) answer_remove_list(answer, user) end + # rubocop:disable Metrics/AbcSize def email_trigger_list(answer) email_list = [] return email_list unless answer.question.option_based? @@ -60,11 +57,12 @@ def email_trigger_list(answer) chosen = answer.question_option_ids.sort next unless chosen == opts - email_list << JSON.parse(cond.webhook_data)["email"] if action == "add_webhook" + email_list << JSON.parse(cond.webhook_data)['email'] if action == 'add_webhook' end # uniq because could get same remove id from diff conds - email_list.uniq.join(",") + email_list.uniq.join(',') end + # rubocop:enable Metrics/AbcSize # number of answers in a section after answers updated with conditions def num_section_answers(plan, section) @@ -82,6 +80,7 @@ def num_section_answers(plan, section) end # number of questions in a section after update with conditions + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def num_section_questions(plan, section, phase = nil) # when section and phase are a hash in exports if section.is_a?(Hash) && @@ -103,6 +102,7 @@ def num_section_questions(plan, section, phase = nil) end count end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # returns an array of hashes of section_id, number of section questions, and # number of section answers @@ -134,7 +134,8 @@ def section_info(plan, section) # ... # ] # } - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def later_question_list(question) collection = {} question.section.phase.template.phases.each do |phase| @@ -164,70 +165,67 @@ def later_question_list(question) end collection end - # rubocop:enable Metrics/AbcSize - # rubocop:enable + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def question_title(question) - raw "Qn. " + question.number.to_s + ": " + - truncate(strip_tags(question.text), - length: DISPLAY_LENGTH, - separator: " ", - escape: false) + raw format('Qn. %{question_nbr}: %{title}', + question_nbr: question.number.to_s, + title: truncate(strip_tags(question.text), length: DISPLAY_LENGTH, + separator: ' ', escape: false)) end def section_title(section) - raw "Sec. " + section.number.to_s + ": " + - truncate(strip_tags(section.title), - length: DISPLAY_LENGTH, - separator: " ", - escape: false) + raw format('Sec. %{section_nbr}: %{title}', + section_nbr: section.number.to_s, + title: truncate(strip_tags(section.title), length: DISPLAY_LENGTH, + separator: ' ', escape: false)) end # used when displaying a question while editing the template # converts condition into text - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def condition_to_text(conditions) - return_string = "" + return_string = '' conditions.each do |cond| opts = cond.option_list.map { |opt| QuestionOption.find(opt).text } - return_string += "" unless return_string.empty? - return_string += "
    " + _("Answering") + " " - return_string += opts.join(" and ") - if cond.action_type == "add_webhook" - subject_string = text_formatted(JSON.parse(cond.webhook_data)["subject"]) - return_string += _(" will send an email with subject ") + subject_string + return_string += '
    ' unless return_string.empty? + return_string += "
    #{_('Answering')} " + return_string += opts.join(' and ') + if cond.action_type == 'add_webhook' + subject_string = text_formatted(JSON.parse(cond.webhook_data)['subject']) + return_string += format(_(' will send an email with subject %{subject_name}'), + subject_name: subject_string) else remove_data = cond.remove_data - rems = remove_data.map { |rem| '"' + Question.find(rem).text + '"' } + rems = remove_data.map { |rem| "\"#{Question.find(rem).text}\"" } - return_string += _(" will remove question ") if rems.length == 1 - return_string += _(" will remove questions ") if rems.length > 1 - return_string += rems.join(" and ") + return_string += _(' will remove question ') if rems.length == 1 + return_string += _(' will remove questions ') if rems.length > 1 + return_string += rems.join(' and ') end end - return_string + "
    " + "#{return_string}" end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def text_formatted(object) - if object.is_a?(Integer) # when remove question id - text = Question.find(object).text - elsif object.is_a?(String) # when email subject - text = object - else - pp "type error" - end + text = Question.find(object).text if object.is_a?(Integer) + text = object if object.is_a?(String) + return 'type error' unless text.present? + cleaned_text = text text = ActionController::Base.helpers.truncate(cleaned_text, length: DISPLAY_LENGTH, - separator: " ", escape: false) - _('"') + text + _('"') + separator: ' ', escape: false) + "\"#{text}\"" end # convert a set of conditions into multi-select form + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def conditions_to_param_form(conditions) param_conditions = {} conditions.each do |condition| - title = "condition" + condition[:number].to_s + title = "condition #{condition[:number]}" condition_hash = { title => { question_option_id: condition.option_list, action_type: condition.action_type, @@ -248,6 +246,7 @@ def conditions_to_param_form(conditions) end param_conditions end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength # returns an hash of hashes of webhook data given a condition array def webhook_hash(conditions) @@ -258,6 +257,5 @@ def webhook_hash(conditions) end web_hash end - end # rubocop:enable Metrics/ModuleLength diff --git a/app/helpers/customizable_template_link_helper.rb b/app/helpers/customizable_template_link_helper.rb index 239e1894a5..f0cb62dfd8 100644 --- a/app/helpers/customizable_template_link_helper.rb +++ b/app/helpers/customizable_template_link_helper.rb @@ -1,28 +1,29 @@ # frozen_string_literal: true +# Helper methods for generating links for customizing templates module CustomizableTemplateLinkHelper - # Link to the appropriate customizable template. # Default link name set if name not set which can be overwritten. + # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity def link_to_customizable_template(name, customization, template) name = nil unless name.present? if customization.present? if customization.created_at < template.created_at - name = name.blank? ? _("Transfer customisation") : name + name = name.blank? ? _('Transfer customisation') : name link_to name, org_admin_template_customization_transfers_path(customization.id), - data: { method: "post" } + data: { method: 'post' } else - name = name.blank? ? _("Edit customisation") : name + name = name.blank? ? _('Edit customisation') : name link_to name, org_admin_template_path(id: customization.id) end else - name = name.blank? ? _("Customise") : name + name = name.blank? ? _('Customise') : name link_to name, org_admin_template_customizations_path(template.id), - "data-method": "post" + 'data-method': 'post' end end - + # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity end diff --git a/app/helpers/exports_helper.rb b/app/helpers/exports_helper.rb index 10cdfc9405..a288457477 100644 --- a/app/helpers/exports_helper.rb +++ b/app/helpers/exports_helper.rb @@ -1,20 +1,20 @@ # frozen_string_literal: true +# Helper methods for Plan exports module ExportsHelper - PAGE_MARGINS = { - top: "5", - bottom: "10", - left: "12", - right: "12" + top: '5', + bottom: '10', + left: '12', + right: '12' }.freeze def font_face - @formatting[:font_face].presence || "Arial, Helvetica, Sans-Serif" + @formatting[:font_face].presence || 'Arial, Helvetica, Sans-Serif' end def font_size - @formatting[:font_size].presence || "12" + @formatting[:font_size].presence || '12' end def margin_top @@ -35,7 +35,7 @@ def margin_right def plan_attribution(attribution) attribution = Array(attribution) - prefix = attribution.many? ? _("Creators:") : _("Creator:") + prefix = attribution.many? ? _('Creators:') : _('Creator:') "#{prefix} #{attribution.join(', ')}" end @@ -49,5 +49,4 @@ def get_margin_value_for_side(side) @formatting.dig(:margin, side).presence || PAGE_MARGINS[side] end end - end diff --git a/app/helpers/feature_flag_helper.rb b/app/helpers/feature_flag_helper.rb index 7fa80bd8b3..f3b7646239 100644 --- a/app/helpers/feature_flag_helper.rb +++ b/app/helpers/feature_flag_helper.rb @@ -1,15 +1,14 @@ +# frozen_string_literal: true + +# Helper method to turn system wide feature flag on/off module FeatureFlagHelper - def self.enabled?(feature) - case feature.to_sym + def self.enabled?(feature) + case feature.to_sym - when :on_sandbox - if Rails.application.secrets.on_sandbox.to_s == 'true' - true - else - false - end - else - false - end + when :on_sandbox + Rails.application.secrets.on_sandbox.to_s == 'true' + else + false end -end \ No newline at end of file + end +end diff --git a/app/helpers/feedbacks_helper.rb b/app/helpers/feedbacks_helper.rb index 1189e8c7e1..2b70df69a8 100644 --- a/app/helpers/feedbacks_helper.rb +++ b/app/helpers/feedbacks_helper.rb @@ -1,21 +1,21 @@ # frozen_string_literal: true +# Helper methods for Feedback messages module FeedbacksHelper - def feedback_confirmation_default_subject - _("%{application_name}: Your plan has been submitted for feedback") + _('%{application_name}: Your plan has been submitted for feedback') end def feedback_confirmation_default_message - _("

    Hello %{user_name}.


    Your plan \"%{plan_name}\" has been submitted for feedback from an - administrator at your organisation.
    If you have questions pertaining to this action, please contact us at %{organisation_email}.

    ") + _('

    Hello %{user_name}.

    ' \ + "

    Your plan \"%{plan_name}\" has been submitted for feedback from an + administrator at your organisation. " \ + "If you have questions pertaining to this action, please contact us + at %{organisation_email}.

    ") end def feedback_constant_to_text(text, user, plan, org) - _("#{text}") % { application_name: _(Rails.configuration.branding[:application][:name]), - user_name: user.name(false), - plan_name: plan.title, - organisation_email: org.contact_email } + format(_(text.to_s), application_name: ApplicationService.application_name, user_name: user.name(false), + plan_name: plan.title, organisation_email: org.contact_email) end - end diff --git a/app/helpers/identifier_helper.rb b/app/helpers/identifier_helper.rb index 300b43a50e..fa61ee0e7c 100644 --- a/app/helpers/identifier_helper.rb +++ b/app/helpers/identifier_helper.rb @@ -1,15 +1,14 @@ # frozen_string_literal: true +# Helper methods for displaying Identifiers module IdentifierHelper - def id_for_display(id:, with_scheme_name: true) - return _("None defined") if id.new_record? || id.value.blank? + return _('None defined') if id.new_record? || id.value.blank? without = id.value_without_scheme_prefix - prefix = with_scheme_name ? id.identifier_scheme.description + ": " : "" - return prefix + id.value unless without != id.value && !without.starts_with?("http") + prefix = with_scheme_name ? "#{id.identifier_scheme.description}: " : '' + return prefix + id.value unless without != id.value && !without.starts_with?('http') - link_to "#{prefix} #{without}", id.value, class: "has-new-window-popup-info" + link_to "#{prefix} #{without}", id.value, class: 'has-new-window-popup-info' end - end diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index d0f3715ac5..db15cf24f5 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true +# Helper methods for Languages module LanguagesHelper - def languages - Rails.cache.fetch("languages", expires_in: 1.hour) { Language.sorted_by_abbreviation } + Rails.cache.fetch('languages', expires_in: 1.hour) { Language.sorted_by_abbreviation } end - end diff --git a/app/helpers/mailer_helper.rb b/app/helpers/mailer_helper.rb index 4fa8934371..f133706dd5 100644 --- a/app/helpers/mailer_helper.rb +++ b/app/helpers/mailer_helper.rb @@ -1,50 +1,49 @@ # frozen_string_literal: true +# Helper methods for Emails module MailerHelper - include PermsHelper def tool_name @tool_name ||= ApplicationService.application_name end - def helpdesk_email - @helpdesk_email ||= Rails.configuration.x.organisation.helpdesk_email + def helpdesk_email(org: nil) + org&.helpdesk_email || Rails.configuration.x.organisation.helpdesk_email end # Returns an unordered HTML list with the permissions associated to the user passed def privileges_list(user) - return "" unless user.respond_to?(:perms) && user.perms.respond_to?(:each) + return '' unless user.respond_to?(:perms) && user.perms.respond_to?(:each) names = name_and_text - r = "
      " + r = '
        ' user.perms.each do |p| r += "
      • #{names[p.name.to_sym]}
      • " if names.key?(p.name.to_sym) end - r += "
      " + r += '
    ' end # Returns the messaging for the specified role def role_text(role) if role.administrator? { - type: _("co-owner"), - placeholder1: _("write and edit the plan in a collaborative manner."), - placeholder2: _("You can also grant rights to other collaborators.") + type: _('co-owner'), + placeholder1: _('write and edit the plan in a collaborative manner.'), + placeholder2: _('You can also grant rights to other collaborators.') } elsif role.editor? { - type: _("editor"), - placeholder1: _("write and edit the plan in a collaborative manner."), + type: _('editor'), + placeholder1: _('write and edit the plan in a collaborative manner.'), placeholder2: nil } else { - type: _("read-only"), - placeholder1: _("read the plan and leave comments."), + type: _('read-only'), + placeholder1: _('read the plan and leave comments.'), placeholder2: nil } end end - end diff --git a/app/helpers/manifests_helper.rb b/app/helpers/manifests_helper.rb index ba841a99cb..0e75dc90ec 100644 --- a/app/helpers/manifests_helper.rb +++ b/app/helpers/manifests_helper.rb @@ -39,10 +39,10 @@ # # config.action_view.stylesheet_manifest_resolver = MyCustomResolver.new # +# Helper methods for JS module ManifestsHelper - # The name of the default asset manifest files. - DEFAULT = "application" + DEFAULT = 'application' # The name of the javascript manifest file to load. Defaults to application.js # @@ -65,5 +65,4 @@ def stylesheet_manifest_file DEFAULT end end - end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 0ccaba62af..feebd82668 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,19 +1,18 @@ # frozen_string_literal: true +# Helper methods for Global notifications and Flash messages module NotificationsHelper - # FA html class depending on Notification level # # Returns String def fa_classes(notification) case notification.level - when "warning" - "fa-exclamation-circle" - when "danger" - "fa-times-circle" + when 'warning' + 'fa-exclamation-circle' + when 'danger' + 'fa-times-circle' else - "fa-info-circle" + 'fa-info-circle' end end - end diff --git a/app/helpers/orgs_helper.rb b/app/helpers/orgs_helper.rb index 16372033bb..c3879f1c6c 100644 --- a/app/helpers/orgs_helper.rb +++ b/app/helpers/orgs_helper.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true +# Helper methods for Orgs module OrgsHelper - - EMAIL_PLACEHOLDER = _("[Organisation Contact Email Placeholder]") + EMAIL_PLACEHOLDER = '[Organisation Contact Email Placeholder]' # Sample message for Org feedback form. # @@ -11,22 +11,18 @@ module OrgsHelper # Returns String def sample_message_for_org_feedback_form(org) email = org.contact_email || EMAIL_PLACEHOLDER - _("

    A data librarian from %{org_name} will respond to your request within 48 + format(_("

    A data librarian from %{org_name} will respond to your request within 48 hours. If you have questions pertaining to this action please contact us - at %{organisation_email}.

    ") % { - organisation_email: email, - org_name: org.name - } + at %{organisation_email}.

    "), organisation_email: email, org_name: org.name) end # The preferred logo url for the current configuration. If DRAGONFLY_AWS is true, return # the remote_url, otherwise return the url def logo_url_for_org(org) - if ENV["DRAGONFLY_AWS"] == "true" + if ENV.fetch('DRAGONFLY_AWS', nil) == 'true' org.logo.remote_url else org.logo.url end end - end diff --git a/app/helpers/paginable_helper.rb b/app/helpers/paginable_helper.rb index b0c06e73d6..9939d96b3e 100644 --- a/app/helpers/paginable_helper.rb +++ b/app/helpers/paginable_helper.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true +# Helper methods for Paginable tables module PaginableHelper - include Paginable - end diff --git a/app/helpers/perms_helper.rb b/app/helpers/perms_helper.rb index 7f5602ea9c..0ec548daae 100644 --- a/app/helpers/perms_helper.rb +++ b/app/helpers/perms_helper.rb @@ -1,21 +1,20 @@ # frozen_string_literal: true +# Helper methods for User permissions module PermsHelper - # Returns a hash whose keys are the names associated to Perms and values are # the text to be displayed to the end user def name_and_text { - add_organisations: _('Add organisations'), + add_organisations: _('Add organisations'), change_org_affiliation: _('Change affiliation'), grant_permissions: _('Manage user privileges'), modify_templates: _('Manage templates'), modify_guidance: _('Manage guidance'), use_api: _('API rights'), change_org_details: _('Manage organisation details'), - grant_api_to_orgs: _('Grant API to organisations'), - review_org_plans: '' + grant_api_to_orgs: _('Grant API to organisations'), + review_org_plans: '' # ISSUE313 solution: not to translate empty string } end - end diff --git a/app/helpers/plans_helper.rb b/app/helpers/plans_helper.rb index 397c7c4c41..0b824e366d 100644 --- a/app/helpers/plans_helper.rb +++ b/app/helpers/plans_helper.rb @@ -1,17 +1,17 @@ # frozen_string_literal: true +# Helper methods for Plans module PlansHelper - # display the role of the user for a given plan def display_role(role) if role.creator? - _("Owner") + _('Owner') elsif role.administrator? - _("Co-owner") + _('Co-owner') elsif role.editor? - _("Editor") + _('Editor') elsif role.commenter? - _("Read only") + _('Read only') end end @@ -19,15 +19,13 @@ def display_role(role) def display_visibility(val) case val when 'organisationally_visible' - return "#{_('Organisation')}" + "#{_('Organisation')}" when 'publicly_visible' - return "#{_('Public')}" + "#{_('Public')}" when 'privately_visible' - return "#{_('Private')}" - when 'is_test' - return "#{_('Test')}" - else - return "N/A" + "#{_('Private')}" + else + "#{_('Private')}" # Test Plans end end @@ -37,27 +35,8 @@ def visibility_tooltip(val) _('Organisation: anyone at my organisation can view.') when 'publicly_visible' _('Public: anyone can view.') - when 'privately_visible' - _('Private: restricted to me and people I invite.') - when 'is_test' - _('Test: mock project for testing, practice, or educational purposes.') - else - _('N/A') - end - end - - def visibility_options(val) - case val - when 'organisationally_visible' - _('Organisation') - when 'publicly_visible' - _('Public') - when 'privately_visible' - _('Private') - when 'is_test' - _('Test') else - _('N/A') + _('Private: restricted to me and people I invite.') end end @@ -72,5 +51,4 @@ def display_section?(customization, section, show_custom_sections) display ||= customization && section[:modifiable] && show_custom_sections display end - end diff --git a/app/helpers/sections_helper.rb b/app/helpers/sections_helper.rb index e3bd8e9beb..90fe7b2d9d 100644 --- a/app/helpers/sections_helper.rb +++ b/app/helpers/sections_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Helper methods for Template Sections module SectionsHelper - # HREF attribute value for headers in the section partials. If the section # is modifiable, returns the section path, otherwise the edit section path. # @@ -25,5 +25,4 @@ def header_path_for_section(section, phase, template) def draggable_for_section?(section) section.template.latest? && section.modifiable? end - end diff --git a/app/helpers/settings_template_helper.rb b/app/helpers/settings_template_helper.rb index 0dab8e5682..f1061b0e77 100644 --- a/app/helpers/settings_template_helper.rb +++ b/app/helpers/settings_template_helper.rb @@ -1,22 +1,22 @@ # frozen_string_literal: true +# Helper methods for Settings module SettingsTemplateHelper - # Retrieves an msgstr for a given admin_field + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def admin_field_t(admin_field) - return _("Unknown column name.") if Settings::Template::VALID_ADMIN_FIELDS.include?(admin_field) - return _("Plan Name") if admin_field == "project_name" - return _("Plan ID") if admin_field == "project_identifier" - return _("Grant number") if admin_field == "grant_title" - return _("Principal Investigator / Researcher") if admin_field == "principal_investigator" - return _("Plan Data Contact") if admin_field == "project_data_contact" - return _("Plan Description") if admin_field == "project_description" - return _("Funder") if admin_field == "funder" - return _("Organisation") if admin_field == "institution" - return _("Your ORCID") if admin_field == "orcid" + return _('Unknown column name.') if Settings::Template::VALID_ADMIN_FIELDS.include?(admin_field) + return _('Plan Name') if admin_field == 'project_name' + return _('Plan ID') if admin_field == 'project_identifier' + return _('Grant number') if admin_field == 'grant_title' + return _('Principal Investigator / Researcher') if admin_field == 'principal_investigator' + return _('Plan Data Contact') if admin_field == 'project_data_contact' + return _('Plan Description') if admin_field == 'project_description' + return _('Funder') if admin_field == 'funder' + return _('Organisation') if admin_field == 'institution' + return _('Your ORCID') if admin_field == 'orcid' - _("Unknown column name.") + _('Unknown column name.') end - # rubocop:enable - + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity end diff --git a/app/helpers/super_admin/orgs/merge_helper.rb b/app/helpers/super_admin/orgs/merge_helper.rb index 1c2051fdda..30f1809b3e 100644 --- a/app/helpers/super_admin/orgs/merge_helper.rb +++ b/app/helpers/super_admin/orgs/merge_helper.rb @@ -1,15 +1,13 @@ # frozen_string_literal: true module SuperAdmin - module Orgs - + # Helper methods for Merging Orgs module MergeHelper - def org_column_content(attributes:) - return "No mergeable attributes" unless attributes.present? && attributes.keys.any? + return 'No mergeable attributes' unless attributes.present? && attributes.keys.any? - html = "
      " + html = '
        ' attributes.each_key do |key| html += "
      • #{key}: #{attributes[key]}
      • " end @@ -17,50 +15,45 @@ def org_column_content(attributes:) end # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def column_content(entries:, orcid:) - return _("None") unless entries.present? && entries.any? + return _('None') unless entries.present? && entries.any? - html = "
          " + html = '
            ' entries.each do |entry| text = case entry.class.name - when "Annotation" - [entry.id, entry.text[0..20]].join(" - ") - when "Department" - [entry.id, entry.name].join(" - ") - when "Guidance" - _("Guidance for: %{themes}") % { - themes: entry.themes.collect(&:title).join(", ") - } - when "Identifier" - [entry.identifier_scheme&.name, entry.value].join(" - ") - when "TokenPermissionType" + when 'Annotation' + [entry.id, entry.text[0..20]].join(' - ') + when 'Department' + [entry.id, entry.name].join(' - ') + when 'Guidance' + format(_('Guidance for: %{themes}'), themes: entry.themes.collect(&:title).join(', ')) + when 'Identifier' + [entry.identifier_scheme&.name, entry.value].join(' - ') + when 'TokenPermissionType' entry.token_type.capitalize - when "Tracker" + when 'Tracker' entry.code - when "User" + when 'User' [entry.email, entry.identifier_for_scheme(scheme: orcid)&.value].compact - .join(" - ") + .join(' - ') else - [entry.id, entry.title].join(" - ") + [entry.id, entry.title].join(' - ') end html += "
          • #{text}
          • " end "#{html}
          " end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def merge_column_content(entries:, orcid:, to_org_name:) - return _("Nothing to merge") unless entries.present? && entries.any? + return _('Nothing to merge') unless entries.present? && entries.any? - html = _("

          The following %{object_types} will be moved over to '%{org_name}':

          ") % { - object_types: entries.first.class.name.pluralize, - org_name: to_org_name - } + html = format(_("

          The following %{object_types} will be moved over to '%{org_name}':

          "), + object_types: entries.first.class.name.pluralize, org_name: to_org_name) html + column_content(entries: entries, orcid: orcid) end - end - end - end diff --git a/app/helpers/template_helper.rb b/app/helpers/template_helper.rb index e40a27c36b..807e6c4559 100644 --- a/app/helpers/template_helper.rb +++ b/app/helpers/template_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Helper methods for Templates module TemplateHelper - def template_details_path(template) if template_modifiable?(template) edit_org_admin_template_path(template) @@ -22,7 +22,7 @@ def template_modifiable?(template) template.org_id = current_user.org.id end - def links_to_a_elements(links, separator = ", ") + def links_to_a_elements(links, separator = ', ') a = links.map do |l| "#{l['text']}" end @@ -34,34 +34,26 @@ def links_to_a_elements(links, separator = ", ") # @param hidden [Boolean] should the link be hidden? # @param text [String] text for the link # @param id [String] id for the link element - def direct_link(template, hidden = false, text = nil, id = nil, protocol = 'http') + # rubocop:disable Style/OptionalBooleanParameter + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def direct_link(template, hidden = false, text = nil, id = nil) params = { org: { id: "{ \"id\": #{current_user&.org&.id}, \"name\": \"#{current_user&.org&.name}\" }" }, funder: { id: "{ \"id\": #{template.org&.id}, \"name\": \"#{template.org&.name}\" }" }, template_id: template.id } - cls = text.nil? ? "direct-link" : "direct-link btn btn-default" - style = hidden ? "display: none" : "" + cls = text.nil? ? 'direct-link' : 'direct-link btn btn-default' + style = hidden ? 'display: none' : '' - link_to(plans_url(plan: params, protocol: protocol), method: :post, title: _("Create plan"), + link_to(plans_url(plan: params), method: :post, title: _('Create plan'), class: cls, id: id, style: style) do if text.nil? - "".html_safe + ''.html_safe else text.html_safe end end end - - def visibility_description(val) - case val - when 'organisationally_visible' - _('Organisation: anyone at my organisation can view.') - when 'publicly_visible' - _('Public: anyone can view.') - else - _('N/A') - end - end - + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # rubocop:enable Style/OptionalBooleanParameter end diff --git a/app/helpers/usage_helper.rb b/app/helpers/usage_helper.rb index 673f61dbcb..6748ede7ab 100644 --- a/app/helpers/usage_helper.rb +++ b/app/helpers/usage_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Helper methods for Usage dashboard module UsageHelper - def prep_data_for_yearly_users_chart(data:) default_chart_prep(data: data) end @@ -14,39 +14,39 @@ def prep_data_for_yearly_plans_chart(data:) # for each point on the Y axis (date) so we need to format the information # appropriately by passing along the labels for the Y axis and the datasets # for the X axis - # rubocop:disable Metrics/AbcSize - def prep_data_for_template_plans_chart(data:, subset: "by_template") - last_month = Date.today.last_month.end_of_month.strftime("%b-%y") + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def prep_data_for_template_plans_chart(data:, subset: 'by_template') + last_month = Date.today.last_month.end_of_month.strftime('%b-%y') return { labels: [last_month], datasets: [] }.to_json if data.blank? || data.empty? datasets = {} # Sort this chart's date by date desacending data = data.map { |hash| JSON.parse(hash) } - .sort { |a, b| b["date"] <=> a["date"] } + .sort { |a, b| b['date'] <=> a['date'] } # Extract all of the dates as month abbreviation - year (e.g. Dec-19) - labels = data.map { |rec| prep_date_for_charts(date: rec["date"]) } - + labels = data.map { |rec| prep_date_for_charts(date: rec['date']) } # Loop through the data and organize the datasets by template instead of date data.each do |rec| - date = prep_date_for_charts(date: rec["date"]) + date = prep_date_for_charts(date: rec['date']) rec[subset].each do |template| # We need a placeholder for each month/year - template combo. The # default is to assume that there are zero plans for that month/year + template dflt = { - label: template["name"], + label: template['name'], backgroundColor: random_rgb, data: labels.map { |lbl| { x: 0, y: lbl } } } - template_hash = datasets.fetch(template["name"], dflt) + template_hash = datasets.fetch(template['name'], dflt) # Replace any of the month/year plan counts for this template IF it has # any plans defined template_hash[:data] = template_hash[:data].map do |dat| - dat[:y] == date ? { x: template["count"] + dat[:x], y: dat[:y] } : dat + dat[:y] == date ? { x: template['count'] + dat[:x], y: dat[:y] } : dat end - datasets[template["name"]] = template_hash + datasets[template['name']] = template_hash end end @@ -56,33 +56,35 @@ def prep_data_for_template_plans_chart(data:, subset: "by_template") labels: labels }.to_json end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # rubocop:disable Metrics/AbcSize def plans_per_template_ranges [ - [_("Last month"), Date.today.last_month.end_of_month], - [_("Last 3 months"), Date.today.months_ago(3).end_of_month], - [_("Last 6 months"), Date.today.months_ago(6).end_of_month], - [_("Last 9 months"), Date.today.months_ago(9).end_of_month], - [_("Last 12 months"), Date.today.months_ago(12).end_of_month] + [_('Last month'), Date.today.last_month.end_of_month], + [_('Last 3 months'), Date.today.months_ago(3).end_of_month], + [_('Last 6 months'), Date.today.months_ago(6).end_of_month], + [_('Last 9 months'), Date.today.months_ago(9).end_of_month], + [_('Last 12 months'), Date.today.months_ago(12).end_of_month] ] end + # rubocop:enable Metrics/AbcSize def default_chart_prep(data:) hash = {} data.map { |rec| JSON.parse(rec) }.each do |rec| - date = prep_date_for_charts(date: rec["date"]) - hash[date] = hash.fetch(date, 0) + rec["count"].to_i + date = prep_date_for_charts(date: rec['date']) + hash[date] = hash.fetch(date, 0) + rec['count'].to_i end hash end def prep_date_for_charts(date:) - date.is_a?(Date) ? date.strftime("%b-%y") : Date.parse(date).strftime("%b-%y") + date.is_a?(Date) ? date.strftime('%b-%y') : Date.parse(date).strftime('%b-%y') end def random_rgb "rgb(#{rand(256)},#{rand(256)},#{rand(256)})" end - end diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 1c83822daa..f61459d552 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -26,6 +26,7 @@ import 'bootstrap-select'; import '../src/utils/accordion'; import '../src/utils/autoComplete'; import '../src/utils/externalLink'; +import '../src/utils/modalSearch'; import '../src/utils/outOfFocus'; import '../src/utils/paginable'; import '../src/utils/panelHeading'; @@ -58,6 +59,7 @@ import '../src/plans/index.js.erb'; import '../src/plans/new'; import '../src/plans/share'; import '../src/publicTemplates/show'; +import '../src/researchOutputs/form'; import '../src/roles/edit'; import '../src/shared/createAccountForm'; import '../src/shared/signInForm'; diff --git a/app/javascript/src/answers/edit.js b/app/javascript/src/answers/edit.js index a0fd8d589a..766ccc9386 100644 --- a/app/javascript/src/answers/edit.js +++ b/app/javascript/src/answers/edit.js @@ -185,4 +185,13 @@ $(() => { } datePicker(); + + // Clicking the 'Comments & Guidance' div should toggle the guidance & comments section + $(document).on('click', '.toggle-guidance-section', (e) => { + const target = $(e.currentTarget); + target.parents('.question-body').find('.guidance-section').toggle(); + target.find('span.fa-chevron-right, span.fa-chevron-left') + .toggleClass('fa-chevron-right') + .toggleClass('fa-chevron-left'); + }); }); diff --git a/app/javascript/src/orgs/adminEdit.js b/app/javascript/src/orgs/adminEdit.js index d26c57ba45..c393c39c27 100644 --- a/app/javascript/src/orgs/adminEdit.js +++ b/app/javascript/src/orgs/adminEdit.js @@ -1,7 +1,5 @@ -// TODO: we need to be able to swap in the appropriate locale here +// TODO: we need to be able to swap in the appropriate locale here import 'number-to-text/converters/en-us'; -// XXX: Check TODO and add way of adding locale change -// import 'number-to-text/converters/fr-ca'; import { isObject } from '../utils/isType'; import { Tinymce } from '../utils/tinymce.js.erb'; import { eachLinks } from '../utils/links'; diff --git a/app/javascript/src/researchOutputs/form.js b/app/javascript/src/researchOutputs/form.js new file mode 100644 index 0000000000..b454b9eb3d --- /dev/null +++ b/app/javascript/src/researchOutputs/form.js @@ -0,0 +1,45 @@ +import getConstant from '../utils/constants'; +import { isUndefined, isObject } from '../utils/isType'; +import { Tinymce } from '../utils/tinymce.js.erb'; + +$(() => { + const form = $('.research_output_form'); + + if (!isUndefined(form) && isObject(form)) { + Tinymce.init({ selector: '#research_output_description' }); + } + + // Expands/Collapses the search results 'More info'/'Less info' section + $('body').on('click', '.modal-search-result .more-info a.more-info-link', (e) => { + e.preventDefault(); + const link = $(e.target); + + if (link.length > 0) { + const info = $(link).siblings('div.info'); + + if (info.length > 0) { + if (info.hasClass('hidden')) { + info.removeClass('hidden'); + link.text(`${getConstant('LESS_INFO')}`); + } else { + info.addClass('hidden'); + link.text(`${getConstant('MORE_INFO')}`); + } + } + } + }); + + // Put the facet text into the modal search window's search box when the user + // clicks on one + $('body').on('click', '.modal-search-result a.facet', (e) => { + const link = $(e.target); + + if (link.length > 0) { + const textField = link.closest('.modal-body').find('input.autocomplete'); + + if (textField.length > 0) { + textField.val(link.text()); + } + } + }); +}); diff --git a/app/javascript/src/utils/links.js b/app/javascript/src/utils/links.js index 900564a51a..b4b8f77721 100644 --- a/app/javascript/src/utils/links.js +++ b/app/javascript/src/utils/links.js @@ -1,4 +1,7 @@ -import writtenNumber from 'written-number'; +// 3.1.0: /* global i18nLocale */... part is confirmed to be removed + +import 'number-to-text/converters/en-us'; +import { convertToText } from 'number-to-text/index'; import { isFunction } from './isType'; const getLinks = (elem) => $(elem).find('.link').map((i, el) => { @@ -88,10 +91,7 @@ $(() => { $('.links').find('.max-number-links').each((i, el) => { const target = $(el); const max = target.closest('.links').attr('data-max-number-links'); - /* global i18nLocale */ - // defined in application.html.erb - const language = i18nLocale.split('-')[0]; - target.text(writtenNumber(max, { lang: language })); + target.text(convertToText(max).toLowerCase()); }); }); diff --git a/app/javascript/src/utils/modalSearch.js b/app/javascript/src/utils/modalSearch.js new file mode 100644 index 0000000000..2bc8bf1888 --- /dev/null +++ b/app/javascript/src/utils/modalSearch.js @@ -0,0 +1,39 @@ +$(() => { + // Add the selected item to the selections section + $('body').on('click', 'a.modal-search-result-selector', (e) => { + e.preventDefault(); + const link = $(e.target); + + if (link.length > 0) { + const selectedBlock = $(e.target).closest('.modal-search-result'); + const resultsBlock = $(e.target).closest('.modal-search-results'); + + if (resultsBlock.length > 0 && selectedBlock.length > 0) { + const selectionsBlockId = resultsBlock.attr('id').replace('-results', '-selections'); + + if (selectionsBlockId !== undefined) { + const selectionsBlock = $(`#${selectionsBlockId}`); + + if (selectionsBlock.length > 0) { + const clone = selectedBlock.clone(); + clone.find('.modal-search-result-selector').addClass('hidden'); + clone.find('.modal-search-result-unselector').removeClass('hidden'); + clone.find('.tags').remove(); + selectionsBlock.append(clone); + selectedBlock.remove(); + } + } + } + } + }); + + // Remove the selected item + $('body').on('click', 'a.modal-search-result-unselector', (e) => { + e.preventDefault(); + const selection = $(e.target).closest('.modal-search-result'); + + if (selection.length > 0) { + selection.remove(); + } + }); +}); diff --git a/app/javascript/src/utils/tinymce.js.erb b/app/javascript/src/utils/tinymce.js.erb index 63c73717a6..cff2bfccca 100644 --- a/app/javascript/src/utils/tinymce.js.erb +++ b/app/javascript/src/utils/tinymce.js.erb @@ -9,8 +9,6 @@ import 'tinymce/plugins/autoresize'; import 'tinymce/plugins/link'; import 'tinymce/plugins/paste'; import 'tinymce/plugins/advlist'; -import 'tinymce-i18n/langs/en_CA'; -import 'tinymce-i18n/langs/fr_FR'; // Other dependencies import { isObject, isString } from './isType'; @@ -31,7 +29,7 @@ export const defaultOptions = { target_list: false, elementpath: false, resize: true, - min_height: 230, + autoresize_min_height: 230, autoresize_bottom_margin: 10, branding: false, extended_valid_elements: 'iframe[tooltip] , a[href|target=_blank]', @@ -48,7 +46,6 @@ export const defaultOptions = { // editorManager.baseURL is not resolved properly for IE since document.currentScript // is not supported, see issue https://github.com/tinymce/tinymce/issues/358 skin_url: '/tinymce/skins/lightgray', - // content_css: '/tinymce/skins/lightgray/content.min.css' content_css: ['/tinymce/tinymce.css'], }; /* @@ -108,9 +105,9 @@ export const Tinymce = { let myDefaultOptions = $.extend(true, {language: tinymceLocale}, defaultOptions) if (isObject(options)) { - tinymce.init($.extend(true, myDefaultOptions, options)).then(resizeEditors); + tinymce.init($.extend(true, defaultOptions, options)).then(resizeEditors); } else { - tinymce.init(myDefaultOptions).then(resizeEditors); + tinymce.init(defaultOptions).then(resizeEditors); } // Connect the label to the Tinymce iframe diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index db81880938..d92ffddcb5 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true class ApplicationJob < ActiveJob::Base - end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index be1df3b698..56267ee547 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true +# Mailer methods for all emails class UserMailer < ActionMailer::Base - - prepend_view_path "app/views/branded/" + prepend_view_path 'app/views/branded/' include MailerHelper helper MailerHelper @@ -10,23 +10,24 @@ class UserMailer < ActionMailer::Base default from: Rails.configuration.x.organisation.email + # rubocop:disable Metrics/AbcSize def welcome_notification(user) @user = user @username = @user.name - @email_subject = _("Query or feedback related to %{tool_name}") % { tool_name: tool_name } + @email_subject = format(_('Query or feedback related to %{tool_name}'), tool_name: tool_name) # Override the default Rails route helper for the contact_us page IF an alternate contact_us # url was defined in the dmproadmap.rb initializer file @contact_us = Rails.application.config.x.organisation.contact_us_url || contact_us_url + @helpdesk_email = helpdesk_email(org: @user.org) I18n.with_locale I18n.default_locale do mail(to: @user.email, - subject: _("Welcome to %{tool_name}") % - { - tool_name: tool_name - }) + subject: format(_('Welcome to %{tool_name}'), tool_name: tool_name)) end end + # rubocop:enable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize def question_answered(data, user, answer, _options_string) @user = user @username = @user.name @@ -35,15 +36,17 @@ def question_answered(data, user, answer, _options_string) @plan_title = @answer.plan.title.to_s @template_title = @answer.plan.template.title.to_s @data = data - @recipient_name = @data["name"].to_s - @message = @data["message"].to_s + @recipient_name = @data['name'].to_s + @message = @data['message'].to_s @answer_text = @options_string.to_s + @helpdesk_email = helpdesk_email(org: @user.org) I18n.with_locale I18n.default_locale do - mail(to: data["email"], - subject: data["subject"]) + mail(to: data['email'], + subject: data['subject']) end end + # rubocop:enable Metrics/AbcSize def sharing_notification(role, user, inviter:) @role = role @@ -51,14 +54,13 @@ def sharing_notification(role, user, inviter:) @user_email = @user.email @username = @user.name @inviter = inviter - @link = url_for(action: "show", controller: "plans", id: @role.plan.id) + @link = url_for(action: 'show', controller: 'plans', id: @role.plan.id) + @helpdesk_email = helpdesk_email(org: @inviter.org) I18n.with_locale I18n.default_locale do mail(to: @role.user.email, - subject: _("A Data Management Plan in %{tool_name} has been shared with you") % - { - tool_name: tool_name - }) + subject: format(_('A Data Management Plan in %{tool_name} has been shared with you'), + tool_name: tool_name)) end end @@ -68,15 +70,14 @@ def permissions_change_notification(role, user) @role = role @plan_title = @role.plan.title @user = user - @username = @user.name - @messaging = role_text(@role) + @recepient = @role.user + @messaging = role_text(@role) + @helpdesk_email = helpdesk_email(org: @user.org) I18n.with_locale I18n.default_locale do - mail(to: @role.user.email, - subject: _("Changed permissions on a Data Management Plan in %{tool_name}") % - { - tool_name: tool_name - }) + mail(to: @recepient.email, + subject: format(_('Changed permissions on a Data Management Plan in %{tool_name}'), + tool_name: tool_name)) end end @@ -86,13 +87,12 @@ def plan_access_removed(user, plan, current_user) @user = user @plan = plan @current_user = current_user + @helpdesk_email = helpdesk_email(org: @plan.org) I18n.with_locale I18n.default_locale do mail(to: @user.email, - subject: _("Permissions removed on a DMP in %{tool_name}") % - { - tool_name: tool_name - }) + subject: format(_('Permissions removed on a DMP in %{tool_name}'), + tool_name: tool_name)) end end @@ -105,16 +105,16 @@ def feedback_notification(recipient, plan, requestor) @recipient_name = @recipient.name(false) @requestor_name = @user.name(false) @plan_name = @plan.title - + @helpdesk_email = helpdesk_email(org: @plan.org) + I18n.with_locale I18n.default_locale do mail(to: @recipient.email, - subject: _("%{user_name} has requested feedback on a %{tool_name} plan") % - { - tool_name: tool_name, user_name: @user.name(false) - }) + subject: format(_('%{user_name} has requested feedback on a %{tool_name} plan'), + tool_name: tool_name, user_name: @user.name(false))) end end + # rubocop:disable Metrics/AbcSize def feedback_complete(recipient, plan, requestor) return unless recipient.active? @@ -124,6 +124,7 @@ def feedback_complete(recipient, plan, requestor) @plan = plan @phase = @plan.phases.first @plan_name = @plan.title + @helpdesk_email = helpdesk_email(org: @plan.org) I18n.with_locale I18n.default_locale do sender = Rails.configuration.x.organisation.do_not_reply_email || @@ -131,12 +132,11 @@ def feedback_complete(recipient, plan, requestor) mail(to: recipient.email, from: sender, - subject: _("%{tool_name}: Expert feedback has been provided for %{plan_title}") % - { - tool_name: tool_name, plan_title: @plan.title - }) + subject: format(_('%{tool_name}: Expert feedback has been provided for %{plan_title}'), + tool_name: tool_name, plan_title: @plan.title)) end end + # rubocop:enable Metrics/AbcSize def plan_visibility(user, plan) return unless user.active? @@ -146,13 +146,11 @@ def plan_visibility(user, plan) @plan = plan @plan_title = @plan.title @plan_visibility = Plan::VISIBILITY_MESSAGE[@plan.visibility.to_sym] + @helpdesk_email = helpdesk_email(org: @plan.org) I18n.with_locale I18n.default_locale do mail(to: @user.email, - subject: _("DMP Visibility Changed: %{plan_title}") % - { - plan_title: @plan.title - }) + subject: format(_('DMP Visibility Changed: %{plan_title}'), plan_title: @plan.title)) end end @@ -176,15 +174,13 @@ def new_comment(commenter, plan, answer) @question_number = @question.number @section_title = @question.section.title @phase_id = @question.section.phase.id - @phase_link = url_for(action: "edit", controller: "plans", id: @plan.id, phase_id: @phase_id) + @phase_link = url_for(action: 'edit', controller: 'plans', id: @plan.id, phase_id: @phase_id) + @helpdesk_email = helpdesk_email(org: @plan.org) I18n.with_locale I18n.default_locale do mail(to: @plan.owner.email, - subject: _("%{tool_name}: A new comment was added to %{plan_title}") % - { - tool_name: tool_name, - plan_title: @plan.title - }) + subject: format(_('%{tool_name}: A new comment was added to %{plan_title}'), + tool_name: tool_name, plan_title: @plan.title)) end end # rubocop:enable Metrics/AbcSize @@ -195,16 +191,16 @@ def admin_privileges(user) @user = user @username = @user.name @ul_list = privileges_list(@user) + @helpdesk_email = helpdesk_email(org: @user.org) I18n.with_locale I18n.default_locale do mail(to: user.email, - subject: _("Administrator privileges granted in %{tool_name}") % - { - tool_name: tool_name - }) + subject: format(_('Administrator privileges granted in %{tool_name}'), + tool_name: tool_name)) end end + # rubocop:disable Metrics/AbcSize def api_credentials(api_client) @api_client = api_client return unless @api_client.contact_email.present? @@ -213,13 +209,12 @@ def api_credentials(api_client) @name = @api_client.contact_name.present? ? @api_client.contact_name : @api_client.contact_email + @helpdesk_email = helpdesk_email(org: @api_client.org) + I18n.with_locale I18n.default_locale do mail(to: @api_client.contact_email, - subject: _("%{tool_name} API changes") % - { - tool_name: tool_name - }) + subject: format(_('%{tool_name} API changes'), tool_name: tool_name)) end end - + # rubocop:enable Metrics/AbcSize end diff --git a/app/models/annotation.rb b/app/models/annotation.rb index 2043e1f0a6..d5799565b4 100644 --- a/app/models/annotation.rb +++ b/app/models/annotation.rb @@ -25,8 +25,8 @@ # fk_rails_... (question_id => questions.id) # +# Object that represents Question level guidance or example answers class Annotation < ApplicationRecord - include VersionableModel ## @@ -85,5 +85,4 @@ def deep_copy(**options) copy.question_id = options.fetch(:question_id, nil) copy end - end diff --git a/app/models/answer.rb b/app/models/answer.rb index c994391b04..d1ff70e9b3 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -28,8 +28,8 @@ # fk_rails_... (user_id => users.id) # +# Object that represents an Answer to a Plan question class Answer < ApplicationRecord - # ================ # = Associations = # ================ @@ -42,7 +42,7 @@ class Answer < ApplicationRecord has_many :notes, dependent: :destroy - has_and_belongs_to_many :question_options, join_table: "answers_question_options" + has_and_belongs_to_many :question_options, join_table: 'answers_question_options' has_many :notes @@ -96,7 +96,7 @@ def answered? # If the question is option based then see if any options were selected return question_options.any? if question.question_format.option_based? # Strip out any white space and see if the text is empty - return !text.gsub(%r{}, "").gsub(%r{}, "").chomp.blank? if text.present? + return !text.gsub(%r{}, '').gsub(%r{}, '').chomp.blank? if text.present? false end @@ -113,7 +113,7 @@ def non_archived_notes # # Returns Hash def answer_hash - default = { "standards" => {}, "text" => "" } + default = { 'standards' => {}, 'text' => '' } begin h = text.nil? ? default : JSON.parse(text) rescue JSON::ParserError @@ -130,10 +130,10 @@ def answer_hash # text - A String with option comment text # # Returns String - def update_answer_hash(standards = {}, text = "") + def update_answer_hash(standards = {}, text = '') h = {} - h["standards"] = standards - h["text"] = text + h['standards'] = standards + h['text'] = text self.text = h.to_json end @@ -152,5 +152,4 @@ def set_plan_complete plan.touch end end - end diff --git a/app/models/api_client.rb b/app/models/api_client.rb index ad521b497c..0c20193c53 100644 --- a/app/models/api_client.rb +++ b/app/models/api_client.rb @@ -24,8 +24,8 @@ # # fk_rails_... (org_id => orgs.id) +# Object that represents an external system class ApiClient < ApplicationRecord - include DeviseInvitable::Inviter extend UniqueRandom @@ -39,9 +39,9 @@ class ApiClient < ApplicationRecord has_many :plans # If the Client_id or client_secret are nil generate them - attribute :client_id, :string, default: -> { unique_random(field_name: "client_id") } + attribute :client_id, :string, default: -> { unique_random(field_name: 'client_id') } attribute :client_secret, :string, - default: -> { unique_random(field_name: "client_secret") } + default: -> { unique_random(field_name: 'client_secret') } # =============== # = Validations = @@ -83,8 +83,7 @@ def authenticate(secret:) # Generate UUIDs for the client_id and client_secret def generate_credentials - self.client_id = ApiClient.unique_random(field_name: "client_id") - self.client_secret = ApiClient.unique_random(field_name: "client_secret") + self.client_id = ApiClient.unique_random(field_name: 'client_id') + self.client_secret = ApiClient.unique_random(field_name: 'client_secret') end - end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 0059011d4d..833a2e34e3 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,17 +1,42 @@ # frozen_string_literal: true +# Base ActiveRecord object class ApplicationRecord < ActiveRecord::Base - include GlobalHelpers include ValidationValues include ValidationMessages self.abstract_class = true + class << self + # Indicates whether the underlying DB is MySQL + def mysql_db? + ActiveRecord::Base.connection.adapter_name == 'Mysql2' + end + + def postgres_db? + ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' + end + + # Generates the appropriate where clause for a JSON field based on the DB type + def safe_json_where_clause(column:, hash_key:) + return "(#{column}->>'#{hash_key}' LIKE ?)" if postgres_db? + + # return "#{column} LIKE ?)" if maria_db? + "(#{column}->>'$.#{hash_key}' LIKE ?)" + end + + # Generates the appropriate where clause for a regular expression based on the DB type + def safe_regexp_where_clause(column:) + return "#{column} ~* ?" unless mysql_db? + + "#{column} REGEXP ?" + end + end + def sanitize_fields(*attrs) attrs.each do |attr| send("#{attr}=", ActionController::Base.helpers.sanitize(send(attr))) end end - end diff --git a/app/models/concerns/acts_as_sortable.rb b/app/models/concerns/acts_as_sortable.rb index 637e50f6df..3ec73d2725 100644 --- a/app/models/concerns/acts_as_sortable.rb +++ b/app/models/concerns/acts_as_sortable.rb @@ -1,29 +1,27 @@ # frozen_string_literal: true +# Module that allows us to sort query results for paginated tables +# rubocop:disable Metrics/BlockLength module ActsAsSortable - extend ActiveSupport::Concern - module ClassMethods - + class_methods do def update_numbers!(ids, parent:) # Ensure only records belonging to this parent are included. ids = ids.map(&:to_i) & parent.public_send("#{model_name.singular}_ids") return if ids.empty? - case connection.adapter_name - when "PostgreSQL" then update_numbers_postgresql!(ids) - when "Mysql2" then update_numbers_mysql2!(ids) - else - update_numbers_sequentially!(ids) - end + update_numbers_postgresql!(ids) if ApplicationRecord.postgres_db? + update_numbers_mysql2!(ids) if ApplicationRecord.mysql_db? + update_numbers_sequentially!(ids) unless ApplicationRecord.postgres_db? || + ApplicationRecord.mysql_db? end private def update_numbers_postgresql!(ids) # Build an Array with each ID and its relative position in the Array - values = ids.each_with_index.map { |id, i| "(#{id}, #{i + 1})" }.join(",") + values = ids.each_with_index.map { |id, i| "(#{id}, #{i + 1})" }.join(',') # Run a single UPDATE query for all records. query = <<~SQL UPDATE #{table_name} \ @@ -35,7 +33,7 @@ def update_numbers_postgresql!(ids) end def update_numbers_mysql2!(ids) - ids_string = ids.map { |id| "'#{id}'" }.join(",") + ids_string = ids.map { |id| "'#{id}'" }.join(',') update_all(%{ number = FIELD(id, #{sanitize_sql(ids_string)}) WHERE id IN (#{sanitize_sql(ids_string)}) }) end @@ -45,7 +43,6 @@ def update_numbers_sequentially!(ids) find(id).update_attribute(:number, number + 1) end end - end - end +# rubocop:enable Metrics/BlockLength diff --git a/app/models/concerns/date_rangeable.rb b/app/models/concerns/date_rangeable.rb index 993aefa769..0589aeea42 100644 --- a/app/models/concerns/date_rangeable.rb +++ b/app/models/concerns/date_rangeable.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true +# Module that allows us to interact with date ranges module DateRangeable - extend ActiveSupport::Concern - module ClassMethods - + class_methods do # Determines whether or not the search term is a date. # Expecting: '[month abbreviation] [year]' e.g.('Oct 2019') def date_range?(term:) @@ -16,10 +15,8 @@ def date_range?(term:) def by_date_range(field, term) date = Date.parse(term) if term[0..1].match(/[0-9]{2}/).present? date = Date.parse("1st #{term}") unless date.present? - query = "%{table}.%{field} BETWEEN ? AND ?" % { table: table_name, field: field } + query = format('%{table}.%{field} BETWEEN ? AND ?', table: table_name, field: field) where(query, date, date.end_of_month) end - end - end diff --git a/app/models/concerns/exportable_plan.rb b/app/models/concerns/exportable_plan.rb index 1d4d516722..f754802a8d 100644 --- a/app/models/concerns/exportable_plan.rb +++ b/app/models/concerns/exportable_plan.rb @@ -3,16 +3,20 @@ # TODO: This code here doesn't make a lot of sense as a Concern since no other model would # ever use the functionality. It would be better to make it a Service. +# Module that provides helper methods for exporting a Plan in various formats # rubocop:disable Metrics/ModuleLength module ExportablePlan - include ConditionsHelper + # rubocop:disable Style/OptionalBooleanParameter def as_pdf(user, coversheet = false) prepare(user, coversheet) end + # rubocop:enable Style/OptionalBooleanParameter - # rubocop:disable Metrics/AbcSize, Metrics/ParameterLists + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # rubocop:disable Style/OptionalBooleanParameter def as_csv(user, headings = true, unanswered = true, @@ -23,11 +27,11 @@ def as_csv(user, CSV.generate do |csv| prepare_coversheet_for_csv(csv, headings, hash) if show_coversheet - hdrs = (hash[:phases].many? ? [_("Phase")] : []) + hdrs = (hash[:phases].many? ? [_('Phase')] : []) hdrs << if headings - [_("Section"), _("Question"), _("Answer")] + [_('Section'), _('Question'), _('Answer')] else - [_("Answer")] + [_('Answer')] end customization = hash[:customization] @@ -48,17 +52,20 @@ def as_csv(user, end end end - # rubocop:enable Metrics/AbcSize, Metrics/ParameterLists + # rubocop:enable Style/OptionalBooleanParameter + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity private # rubocop:disable Metrics/MethodLength, Metrics/AbcSize + # rubocop:disable Style/OptionalBooleanParameter def prepare(user, coversheet = false) hash = coversheet ? prepare_coversheet : {} template = Template.includes(phases: { sections: { questions: :question_format } }) .joins(phases: { sections: { questions: :question_format } }) .where(id: template_id) - .order("sections.number", "questions.number").first + .order('sections.number', 'questions.number').first hash[:customization] = template.customization_of.present? hash[:title] = title hash[:answers] = answers @@ -90,27 +97,27 @@ def prepare(user, coversheet = false) hash end - # rubocop:enable Metrics/MethodLength, Metrics/AbcSize - # rubocop:disable Metrics/AbcSize + # rubocop:enable Style/OptionalBooleanParameter + + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def prepare_coversheet hash = {} - # name of owner and any co-owners - attribution = owner.present? ? [owner.name(false)] : [] - roles.administrator.not_creator.each do |role| - attribution << role.user.name(false) - end + # Use the name of the DMP owner/creator OR the first Co-owner if there is no + # owner for some reason + attribution = roles.creator.first&.user&.name(false) + roles.administrator.not_creator.first&.user&.name(false) unless attribution.present? hash[:attribution] = attribution # Added contributors to coverage of plans. # Users will see both roles and contributor names if the role is filled hash[:data_curation] = Contributor.where(plan_id: id).data_curation hash[:investigation] = Contributor.where(plan_id: id).investigation - hash[:pa] = Contributor.where(plan_id: id).project_administration + hash[:pa] = Contributor.where(plan_id: id).project_administration hash[:other] = Contributor.where(plan_id: id).other # Org name of plan owner's org - hash[:affiliation] = owner.present? ? owner.org.name : "" + hash[:affiliation] = owner.present? ? owner.org.name : '' # set the funder name hash[:funder] = funder.name if funder.present? @@ -119,56 +126,52 @@ def prepare_coversheet # set the template name and customizer name if applicable hash[:template] = template.title - customizer = "" + customizer = '' cust_questions = questions.where(modifiable: true).pluck(:id) # if the template is customized, and has custom answered questions if template.customization_of.present? && Answer.where(plan_id: id, question_id: cust_questions).present? - customizer = _(" Customised By: ") + template.org.name + customizer = _(' Customised By: ') + template.org.name end hash[:customizer] = customizer hash end # rubocop:enable Metrics/AbcSize - # rubocop:enable - # rubocop:disable Metrics/MethodLength, Metrics/AbcSize + # rubocop:disable Metrics/AbcSize def prepare_coversheet_for_csv(csv, _headings, hash) - csv << [_("Title: "), _("%{title}") % { title: title }] - csv << [if hash[:attribution].many? - _("Creators: ") - else - _("Creator:") - end, _("%{authors}") % { authors: hash[:attribution].join(", ") }] + csv << [_('Title: '), format(_('%{title}'), title: title)] + csv << if Array(hash[:attribution]).many? + [_('Creators: '), format(_('%{authors}'), authors: Array(hash[:attribution]).join(', '))] + else + [_('Creator:'), format(_('%{authors}'), authors: hash[:attribution])] + end if hash[:investigation].present? - csv << [_("Principal Investigator: "), - _("%{investigation}") % { investigation: hash[:investigation].map(&:name).join(", ") }] + csv << [_('Principal Investigator: '), + format(_('%{investigation}'), investigation: hash[:investigation].map(&:name).join(', '))] end if hash[:data_curation].present? - csv << [_("Date Manager: "), - _("%{data_curation}") % { data_curation: hash[:data_curation].map(&:name).join(", ") }] + csv << [_('Date Manager: '), + format(_('%{data_curation}'), data_curation: hash[:data_curation].map(&:name).join(', '))] end if hash[:pa].present? - csv << [_("Project Administrator: "), _("%{pa}") % { pa: hash[:pa].map(&:name).join(", ") }] + csv << [_('Project Administrator: '), format(_('%{pa}'), pa: hash[:pa].map(&:name).join(', '))] end if hash[:other].present? - csv << [_("Contributor: "), _("%{other}") % { other: hash[:other].map(&:name).join(", ") }] + csv << [_('Contributor: '), format(_('%{other}'), other: hash[:other].map(&:name).join(', '))] end - csv << [_("Affiliation: "), _("%{affiliation}") % { affiliation: hash[:affiliation] }] + csv << [_('Affiliation: '), format(_('%{affiliation}'), affiliation: hash[:affiliation])] csv << if hash[:funder].present? - [_("Template: "), _("%{funder}") % { funder: hash[:funder] }] + [_('Template: '), format(_('%{funder}'), funder: hash[:funder])] else - [_("Template: "), _("%{template}") % { template: hash[:template] + hash[:customizer] }] + [_('Template: '), format(_('%{template}'), template: hash[:template] + hash[:customizer])] end - if grant&.value.present? - csv << [_("Grant number: "), _("%{grant_number}") % { grant_number: grant&.value }] - end + csv << [_('Grant number: '), format(_('%{grant_number}'), grant_number: grant&.value)] if grant&.value.present? if description.present? - csv << [_("Project abstract: "), _("%{description}") % - { description: Nokogiri::HTML(description).text }] + csv << [_('Project abstract: '), format(_('%{description}'), description: Nokogiri::HTML(description).text)] end - csv << [_("Last modified: "), _("%{date}") % { date: updated_at.to_date.strftime("%d-%m-%Y") }] - csv << [_("Copyright information:"), + csv << [_('Last modified: '), format(_('%{date}'), date: updated_at.to_date.strftime('%d-%m-%Y'))] + csv << [_('Copyright information:'), _("The above plan creator(s) have agreed that others may use as much of the text of this plan as they would like in their own plans, and customise it as necessary. You do not need to credit the creator(s) @@ -187,23 +190,21 @@ def show_section_for_csv(csv, phase, section, headings, unanswered, hash) next if remove_list(hash).include?(question[:id]) answer = self.answer(question[:id], false) - answer_text = "" + answer_text = '' if answer.present? - if answer.question_options.any? - answer_text += answer.question_options.pluck(:text).join(", ") - end - answer_text += answer.text if answer.answered? + answer_text += answer.question_options.pluck(:text).join(', ') if answer.question_options.any? + answer_text += answer.text if answer.answered? && answer.text.present? elsif unanswered - answer_text += _("Not Answered") + answer_text += _('Not Answered') end - single_line_answer_for_csv = sanitize_text(answer_text).gsub(/\r|\n/, " ") + single_line_answer_for_csv = sanitize_text(answer_text).gsub(/\r|\n/, ' ') flds = (hash[:phases].many? ? [phase[:title]] : []) if headings question_text = if question[:text].is_a? String question[:text] else (if question[:text].many? - question[:text].join(", ") + question[:text].join(', ') else question[:text][0] end) @@ -217,9 +218,10 @@ def show_section_for_csv(csv, phase, section, headings, unanswered, hash) end end # rubocop:enable Metrics/AbcSize, Metrics/BlockLength, Metrics/MethodLength - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # rubocop:enable Metrics/ParameterLists + # rubocop:disable Metrics/AbcSize def record_plan_export(user, format) # TODO: Re-evaluate how/why we are doing this. The only place it is used is in statistics # generation as 'downloads' without any regard for the format (although we only call this @@ -240,10 +242,10 @@ def record_plan_export(user, format) end exported_plan.save end + # rubocop:enable Metrics/AbcSize def sanitize_text(text) - ActionView::Base.full_sanitizer.sanitize(text.to_s.gsub(/ /i, "")) + ActionView::Base.full_sanitizer.sanitize(text.to_s.gsub(/ /i, '')) end - end # rubocop:enable Metrics/ModuleLength diff --git a/app/models/concerns/identifiable.rb b/app/models/concerns/identifiable.rb index 3aea3fc227..e1f0e01eef 100644 --- a/app/models/concerns/identifiable.rb +++ b/app/models/concerns/identifiable.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Module that allows the Model to have identifiers module Identifiable - extend ActiveSupport::Concern # rubocop:disable Metrics/BlockLength @@ -25,6 +25,7 @@ module Identifiable # Expects an array of `identifier_scheme.name` and `identifier.value` # [{ name: "fundref", value: "12345" }, { name: "ror", value: "abc"} ] # Returns an instance of the model + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def self.from_identifiers(array:) return nil unless array.present? && array.any? @@ -44,7 +45,7 @@ def self.from_identifiers(array:) id.present? ? id.identifiable : nil end - # rubocop:enable + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # ==================== # = Instance Methods = @@ -75,5 +76,4 @@ def consolidate_identifiers!(array:) end end # rubocop:enable Metrics/BlockLength - end diff --git a/app/models/concerns/json_link_validator.rb b/app/models/concerns/json_link_validator.rb index e15eaa25d0..544f0e22ff 100644 --- a/app/models/concerns/json_link_validator.rb +++ b/app/models/concerns/json_link_validator.rb @@ -1,18 +1,17 @@ # frozen_string_literal: true -module JSONLinkValidator - +# Module that helps validate Template and Org links +module JsonLinkValidator # Validates whether or not the value passed is conforming to # [{ link: String, text: String}, ...] def valid_links?(value) if value.is_a?(Array) r = value.all? do |o| - o.is_a?(Hash) && o.key?("link") && o.key?("text") && - o["link"].is_a?(String) && o["text"].is_a?(String) + o.is_a?(Hash) && o.key?('link') && o.key?('text') && + o['link'].is_a?(String) && o['text'].is_a?(String) end return r end false end - end diff --git a/app/models/concerns/validation_messages.rb b/app/models/concerns/validation_messages.rb index 1fb9b3a918..3cf0bff5cc 100644 --- a/app/models/concerns/validation_messages.rb +++ b/app/models/concerns/validation_messages.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Module that provides consistent validation messaging module ValidationMessages - # workaround for errors thrown by puma when eager loading application # TODO: see if this is still a problem with translations gem # I18n.add_text_domain "app", path: "config/locale", type: :po, @@ -10,12 +10,11 @@ module ValidationMessages PRESENCE_MESSAGE = _("can't be blank") - UNIQUENESS_MESSAGE = _("must be unique") + UNIQUENESS_MESSAGE = _('must be unique') INCLUSION_MESSAGE = _("isn't a valid value") - OPTION_PRESENCE_MESSAGE = _("You must have at least one option with accompanying text.") + OPTION_PRESENCE_MESSAGE = _('You must have at least one option with accompanying text.') QUESTION_TEXT_PRESENCE_MESSAGE = _("for 'Question text' can't be blank.") - end diff --git a/app/models/concerns/validation_values.rb b/app/models/concerns/validation_values.rb index 531e1237eb..3f06ed743f 100644 --- a/app/models/concerns/validation_values.rb +++ b/app/models/concerns/validation_values.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true +# module that provides default validation values module ValidationValues - BOOLEAN_VALUES = [true, false].freeze - end diff --git a/app/models/concerns/versionable_model.rb b/app/models/concerns/versionable_model.rb index a972ab677d..91649ca8a2 100644 --- a/app/models/concerns/versionable_model.rb +++ b/app/models/concerns/versionable_model.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Module that allows a Model to be versioned module VersionableModel - extend ActiveSupport::Concern included do @@ -12,8 +12,7 @@ module VersionableModel attribute :versionable_id, :string, default: lambda { - unique_uuid(field_name: "versionable_id") + unique_uuid(field_name: 'versionable_id') } end - end diff --git a/app/models/condition.rb b/app/models/condition.rb index 6fd4859b6d..ef0d79cca3 100644 --- a/app/models/condition.rb +++ b/app/models/condition.rb @@ -24,8 +24,8 @@ # # +# Object that represents a condition of a conditional question class Condition < ApplicationRecord - belongs_to :question enum action_type: %i[remove add_webhook] serialize :option_list, Array @@ -42,5 +42,4 @@ def deep_copy(**options) copy.save!(validate: false) if options.fetch(:save, false) copy end - end diff --git a/app/models/contributor.rb b/app/models/contributor.rb index f177935ad9..2e76f308a1 100644 --- a/app/models/contributor.rb +++ b/app/models/contributor.rb @@ -26,8 +26,8 @@ # fk_rails_... (org_id => orgs.id) # fk_rails_... (plan_id => plans.id) +# Object that represents a contributor to a plan class Contributor < ApplicationRecord - include FlagShihTzu include ValidationMessages include Identifiable @@ -53,13 +53,13 @@ class Contributor < ApplicationRecord validates :roles, presence: { message: PRESENCE_MESSAGE } validates :roles, numericality: { greater_than: 0, - message: _("You must specify at least one role.") } + message: _('You must specify at least one role.') } validate :name_or_email_presence - ONTOLOGY_NAME = "CRediT - Contributor Roles Taxonomy" - ONTOLOGY_LANDING_PAGE = "https://credit.niso.org/" - ONTOLOGY_BASE_URL = "http://credit.niso.org/contributor-roles/" + ONTOLOGY_NAME = 'CRediT - Contributor Roles Taxonomy' + ONTOLOGY_LANDING_PAGE = 'https://credit.niso.org/' + ONTOLOGY_BASE_URL = 'http://credit.niso.org/contributor-roles/' ## # Define Bit Field values for roles @@ -68,7 +68,7 @@ class Contributor < ApplicationRecord 2 => :investigation, 3 => :project_administration, 4 => :other, - column: "roles" + column: 'roles' # ========== # = Scopes = @@ -89,28 +89,28 @@ class Contributor < ApplicationRecord # ======================== class << self - # returns the default role def default_role - "other" + 'other' end - end # Check for equality by matching on Plan, ORCID, email or name + # rubocop:disable Metrics/CyclomaticComplexity def ==(other) return false unless other.is_a?(Contributor) && plan == other.plan - current_orcid = identifier_for_scheme(scheme: "orcid")&.value - new_orcid = other.identifier_for_scheme(scheme: "orcid")&.value + current_orcid = identifier_for_scheme(scheme: 'orcid')&.value + new_orcid = other.identifier_for_scheme(scheme: 'orcid')&.value email == other.email || name == other.name || (current_orcid.present? && current_orcid == new_orcid) end + # rubocop:enable Metrics/CyclomaticComplexity # Merges the contents of the other Contributor into this one while retaining # any existing information - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def merge(other) self.org = other.org unless org.present? self.email = other.email unless email.present? @@ -122,7 +122,7 @@ def merge(other) consolidate_identifiers!(array: other.identifiers.to_a) self end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # =================== # = Private Methods = @@ -130,11 +130,20 @@ def merge(other) private + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity def name_or_email_presence - return true unless name.blank? && email.blank? + errors.add(:name, _("can't be blank.")) if name.blank? && Rails.configuration.x.application.require_contributor_name + if email.blank? && Rails.configuration.x.application.require_contributor_email + errors.add(:email, + _("can't be blank.")) + end - errors.add(:name, _("can't be blank if no email is provided")) - errors.add(:email, _("can't be blank if no name is provided")) - end + if name.blank? && email.blank? && errors.empty? + errors.add(:name, _("can't be blank if no email is provided.")) + errors.add(:email, _("can't be blank if no name is provided.")) + end + errors.empty? + end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity end diff --git a/app/models/department.rb b/app/models/department.rb index 8733cf0174..a3c2a189a3 100644 --- a/app/models/department.rb +++ b/app/models/department.rb @@ -16,8 +16,8 @@ # index_departments_on_org_id (org_id) # +# Object that a department within an Org class Department < ApplicationRecord - belongs_to :org has_many :users, dependent: :nullify @@ -37,5 +37,4 @@ class Department < ApplicationRecord # Retrieves every department associated to an org scope :by_org, ->(org) { where(org_id: org.id) } - end diff --git a/app/models/exported_plan.rb b/app/models/exported_plan.rb index f26d2b140b..66cd73f259 100644 --- a/app/models/exported_plan.rb +++ b/app/models/exported_plan.rb @@ -12,8 +12,9 @@ # plan_id :integer # user_id :integer # -class ExportedPlan < ApplicationRecord +# Object that records when/how a plan was exported/downloaded +class ExportedPlan < ApplicationRecord include SettingsTemplateHelper # associations between tables @@ -27,7 +28,7 @@ class ExportedPlan < ApplicationRecord # Store settings with the exported plan so it can be recreated later # if necessary (otherwise the settings associated with the plan at a # given time can be lost) - has_settings :export, class_name: "Settings::Template" do |s| + has_settings :export, class_name: 'Settings::Template' do |s| s.key :export, defaults: Settings::Template::DEFAULT_SETTINGS end @@ -80,10 +81,10 @@ def institution end def orcid - return "" unless owner.present? + return '' unless owner.present? - ids = owner.identifiers.by_scheme_name("orcid", "User") - ids.first.present? ? ids.first.value : "" + ids = owner.identifiers.by_scheme_name('orcid', 'User') + ids.first.present? ? ids.first.value : '' end def sections @@ -107,15 +108,16 @@ def title # Export formats # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/BlockLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def as_csv(sections, unanswered_questions, question_headings) CSV.generate do |csv| # rubocop:disable Style/ConditionalAssignment if question_headings - csv << [_("Section"), _("Question"), _("Answer"), _("Selected option(s)"), - _("Answered by"), _("Answered at")] + csv << [_('Section'), _('Question'), _('Answer'), _('Selected option(s)'), + _('Answered by'), _('Answered at')] else - csv << [_("Section"), _("Answer"), _("Selected option(s)"), _("Answered by"), - _("Answered at")] + csv << [_('Section'), _('Answer'), _('Selected option(s)'), _('Answered by'), + _('Answered at')] end # rubocop:enable Style/ConditionalAssignment sections.each do |section| @@ -124,18 +126,18 @@ def as_csv(sections, unanswered_questions, question_headings) # skip unansewered questions next if answer.blank? && !unanswered_questions - answer_text = answer.present? ? answer.text : "" + answer_text = answer.present? ? answer.text : '' q_format = question.question_format options_string = if q_format.option_based? - answer.question_options.collect(&:text).join("; ") + answer.question_options.collect(&:text).join('; ') else - "" + '' end csv << if question_headings [ section.title, sanitize_text(question.text), - question.option_comment_display ? sanitize_text(answer_text) : "", + question.option_comment_display ? sanitize_text(answer_text) : '', options_string, user.name, answer.updated_at @@ -143,7 +145,7 @@ def as_csv(sections, unanswered_questions, question_headings) else [ section.title, - question.option_comment_display ? sanitize_text(answer_text) : "", + question.option_comment_display ? sanitize_text(answer_text) : '', options_string, user.name, answer.updated_at @@ -154,19 +156,20 @@ def as_csv(sections, unanswered_questions, question_headings) end end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/BlockLength - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def as_txt(sections, unanswered_questions, question_headings, details) output = "#{plan.title}\n\n#{plan.template.title}\n" - output += "\n" + _("Details") + "\n\n" + output += "\n#{_('Details')}\n\n" if details admin_details.each do |at| value = send(at) output += if value.present? - admin_field_t(at.to_s) + ": " + value + "\n" + "#{admin_field_t(at.to_s)}: #{value}\n" else - admin_field_t(at.to_s) + ": " + _("-") + "\n" + "#{admin_field_t(at.to_s)}: -\n" end end end @@ -179,11 +182,11 @@ def as_txt(sections, unanswered_questions, question_headings, details) next if answer.nil? && !unanswered_questions if question_headings - qtext = sanitize_text(question.text.gsub(/
        • /, " * ")) + qtext = sanitize_text(question.text.gsub(/
        • /, ' * ')) output += "\n* #{qtext}" end if answer.nil? - output += _("Question not answered.") + "\n" + output += _('Question not answered.\n') else q_format = question.question_format if q_format.option_based? @@ -198,11 +201,12 @@ def as_txt(sections, unanswered_questions, question_headings, details) output end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity private # Returns an Array of question_ids for the exported settings stored for a plan + # rubocop:disable Style/CaseLikeIf def questions question_settings = settings(:export).fields[:questions] @questions ||= if question_settings.present? @@ -217,9 +221,9 @@ def questions [] end end + # rubocop:enable Style/CaseLikeIf def sanitize_text(text) - ActionView::Base.full_sanitizer.sanitize(text.gsub(/ /i, "")) unless text.nil? + ActionView::Base.full_sanitizer.sanitize(text.gsub(/ /i, '')) unless text.nil? end - end diff --git a/app/models/guidance.rb b/app/models/guidance.rb index 65db4e26b1..b8b240b715 100644 --- a/app/models/guidance.rb +++ b/app/models/guidance.rb @@ -33,15 +33,15 @@ # [+Created:+] 07/07/2014 # [+Copyright:+] Digital Curation Centre and California Digital Library +# Object that represents themed guidance class Guidance < ApplicationRecord - # ================ # = Associations = # ================ belongs_to :guidance_group - has_and_belongs_to_many :themes, join_table: "themes_in_guidance" + has_and_belongs_to_many :themes, join_table: 'themes_in_guidance' # =============== # = Validations = @@ -54,7 +54,6 @@ class Guidance < ApplicationRecord validates :published, inclusion: { message: INCLUSION_MESSAGE, in: BOOLEAN_VALUES } - # THEMES validates :themes, presence: { message: PRESENCE_MESSAGE }, if: :published? # Retrieves every guidance associated to an org @@ -65,8 +64,8 @@ class Guidance < ApplicationRecord scope :search, lambda { |term| search_pattern = "%#{term}%" joins(:guidance_group) - .where("lower(guidances.text) LIKE lower(?) OR " \ - "lower(guidance_groups.name) LIKE lower(?)", + .where('lower(guidances.text) LIKE lower(?) OR ' \ + 'lower(guidance_groups.name) LIKE lower(?)', search_pattern, search_pattern) } @@ -85,24 +84,24 @@ class Guidance < ApplicationRecord # user - A User object # # Returns Boolean + # rubocop:disable Metrics/AbcSize def self.can_view?(user, id) guidance = Guidance.find_by(id: id) viewable = false - unless guidance.nil? - unless guidance.guidance_group.nil? - # guidances are viewable if they are owned by the user's org - viewable = true if guidance.guidance_group.org == user.org - # guidance groups are viewable if they are owned by the Default Orgs - viewable = true if Org.default_orgs.include?(guidance.guidance_group.org) + if !guidance.nil? && !guidance.guidance_group.nil? + # guidances are viewable if they are owned by the user's org + viewable = true if guidance.guidance_group.org == user.org + # guidance groups are viewable if they are owned by the Default Orgs + viewable = true if Org.default_orgs.include?(guidance.guidance_group.org) - # guidance groups are viewable if they are owned by a funder - viewable = true if Org.funder.include?(guidance.guidance_group.org) - end + # guidance groups are viewable if they are owned by a funder + viewable = true if Org.funder.include?(guidance.guidance_group.org) end viewable end + # rubocop:enable Metrics/AbcSize # Returns a list of all guidances which a specified user can view # we define guidances viewable to a user by those owned by a guidance group: @@ -138,10 +137,8 @@ def self.all_viewable(user) # # Returns Boolean def in_group_belonging_to?(org_id) - unless guidance_group.nil? - return true if guidance_group.org.id == org_id - end + return true if !guidance_group.nil? && (guidance_group.org.id == org_id) + false end - end diff --git a/app/models/guidance_group.rb b/app/models/guidance_group.rb index c4c2ba04c7..c108cea48e 100644 --- a/app/models/guidance_group.rb +++ b/app/models/guidance_group.rb @@ -24,8 +24,8 @@ # fk_rails_... (org_id => orgs.id) # +# Object that represents a grouping of themed guidance class GuidanceGroup < ApplicationRecord - attribute :optional_subset, :boolean, default: true attribute :published, :boolean, default: false @@ -63,7 +63,7 @@ class GuidanceGroup < ApplicationRecord scope :search, lambda { |term| search_pattern = "%#{term}%" - where("lower(name) LIKE lower(?)", search_pattern) + where('lower(name) LIKE lower(?)', search_pattern) } scope :published, -> { where(published: true) } @@ -137,6 +137,7 @@ def self.create_org_default(org) # = Instance methods = # ==================== + # rubocop:disable Metrics/AbcSize def merge!(to_be_merged:) return self unless to_be_merged.is_a?(GuidanceGroup) @@ -160,5 +161,5 @@ def merge!(to_be_merged:) reload end end - + # rubocop:enable Metrics/AbcSize end diff --git a/app/models/identifier.rb b/app/models/identifier.rb index bd300f7004..0ee44592b1 100644 --- a/app/models/identifier.rb +++ b/app/models/identifier.rb @@ -17,8 +17,9 @@ # # index_identifiers_on_identifiable_type_and_identifiable_id (identifiable_type,identifiable_id) # -class Identifier < ApplicationRecord +# Object that represents an identifier for an object +class Identifier < ApplicationRecord # ================ # = Associations = # ================ @@ -60,13 +61,14 @@ def self.by_scheme_name(scheme, identifiable_type) # Ensure that the value of attrs is a hash # TODO: evaluate this vs the Serialize approach in condition.rb def attrs=(hash) - super(hash.is_a?(Hash) ? hash.to_json.to_s : "{}") + super(hash.is_a?(Hash) ? hash.to_json.to_s : '{}') end # Appends the identifier scheme's prefix to the identifier if necessary # For example: # value '0000-0000-0000-0001' # becomes 'https://orcid.org/0000-0000-0000-0001' + # rubocop:disable Metrics/AbcSize def value=(val) if identifier_scheme.present? && identifier_scheme.identifier_prefix.present? && @@ -74,12 +76,13 @@ def value=(val) !val.to_s.starts_with?(identifier_scheme.identifier_prefix) base = identifier_scheme.identifier_prefix - base += "/" unless base.ends_with?("/") + base += '/' unless base.ends_with?('/') super("#{base}#{val}") else super(val) end end + # rubocop:enable Metrics/AbcSize # =========================== # = Public instance methods = @@ -90,14 +93,14 @@ def identifier_format scheme = identifier_scheme&.name return scheme if %w[orcid ror fundref].include?(scheme) - return "ark" if value.include?("ark:") + return 'ark' if value.include?('ark:') doi_regex = %r{(doi:)?[0-9]{2}\.[0-9]+/.} - return "doi" if value =~ doi_regex + return 'doi' if value =~ doi_regex - return "url" if value.starts_with?("http") + return 'url' if value.starts_with?('http') - "other" + 'other' end # Returns the value sans the identifier scheme's prefix. @@ -109,7 +112,7 @@ def value_without_scheme_prefix identifier_scheme.identifier_prefix.present? base = identifier_scheme.identifier_prefix - value.gsub(base, "").sub(%r{^/}, "") + value.gsub(base, '').sub(%r{^/}, '') end private @@ -128,15 +131,14 @@ def value_uniqueness_without_scheme # if scheme is nil, then just unique for identifiable return unless Identifier.where(identifiable: identifiable, value: value).any? - errors.add(:value, _("must be unique")) + errors.add(:value, _('must be unique')) end # Ensure that the identifiable only has one identifier for the scheme def value_uniqueness_with_scheme if new_record? && Identifier.where(identifier_scheme: identifier_scheme, identifiable: identifiable).any? - errors.add(:identifier_scheme, _("already assigned a value")) + errors.add(:identifier_scheme, _('already assigned a value')) end end - end diff --git a/app/models/identifier_scheme.rb b/app/models/identifier_scheme.rb index 86fb622bc2..ac03e7f6ba 100644 --- a/app/models/identifier_scheme.rb +++ b/app/models/identifier_scheme.rb @@ -15,8 +15,8 @@ # updated_at :datetime # +# Object that represents a type of identifiaction (e.g. ORCID, ROR, etc.) class IdentifierScheme < ApplicationRecord - include FlagShihTzu ## @@ -41,7 +41,7 @@ class IdentifierScheme < ApplicationRecord # =========================== scope :active, -> { where(active: true) } - scope :by_name, ->(value) { active.where("LOWER(name) = LOWER(?)", value) } + scope :by_name, ->(value) { active.where('LOWER(name) = LOWER(?)', value) } ## # Define Bit Field values for the scheme's context @@ -51,7 +51,7 @@ class IdentifierScheme < ApplicationRecord 3 => :for_plans, 4 => :for_users, 5 => :for_contributors, - column: "context", + column: 'context', check_for_column: false # ========================= @@ -62,11 +62,10 @@ class IdentifierScheme < ApplicationRecord # { "ror": "12345" } # so we cannot allow spaces or non alpha characters! def name=(value) - super(value&.downcase&.gsub(/[^a-z]/, "")) + super(value&.downcase&.gsub(/[^a-z]/, '')) end # =========================== # = Instance Methods = # =========================== - end diff --git a/app/models/language.rb b/app/models/language.rb index a62b03cfd7..d08a701911 100644 --- a/app/models/language.rb +++ b/app/models/language.rb @@ -11,8 +11,8 @@ # name :string # +# Object that represents a locale/language class Language < ApplicationRecord - # ============= # = Constants = # ============= @@ -39,7 +39,7 @@ class Language < ApplicationRecord length: { maximum: NAME_MAXIMUM_LENGTH } validates :abbreviation, presence: { message: "can't be blank" }, - uniqueness: { message: "must be unique" }, + uniqueness: { message: 'must be unique' }, length: { maximum: ABBREVIATION_MAXIMUM_LENGTH }, format: { with: ABBREVIATION_FORMAT } @@ -52,7 +52,7 @@ class Language < ApplicationRecord # ensure abbreviation is downcase and conforms to I18n locales # TODO: evaluate the need for the LocaleService after move to Translation.io def abbreviation=(value) - value = "" if value.nil? + value = '' if value.nil? value = value.downcase if value.blank? || value =~ /\A[a-z]{2}\Z/i super(value) @@ -77,11 +77,10 @@ def abbreviation=(value) # ======================== def self.many? - Rails.cache.fetch([model_name, "many?"], expires_in: 1.hour) { all.many? } + Rails.cache.fetch([model_name, 'many?'], expires_in: 1.hour) { all.many? } end def self.default where(default_language: true).first end - end diff --git a/app/models/license.rb b/app/models/license.rb new file mode 100644 index 0000000000..e59f74250d --- /dev/null +++ b/app/models/license.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: licenses +# +# id :bigint not null, primary key +# deprecated :boolean default(FALSE) +# identifier :string not null +# name :string not null +# osi_approved :boolean default(FALSE) +# uri :string not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_license_on_identifier_and_criteria (identifier,osi_approved,deprecated) +# index_licenses_on_identifier (identifier) +# index_licenses_on_uri (uri) +# +class License < ApplicationRecord + # ================ + # = Associations = + # ================ + + has_many :research_outputs + + # ========== + # = Scopes = + # ========== + + scope :selectable, lambda { + where(deprecated: false) + } + + scope :preferred, lambda { + # Fetch the list of preferred license from the config. + preferences = Rails.configuration.x.madmp.preferred_licenses || [] + return selectable unless preferences.is_a?(Array) && preferences.any? + + licenses = preferences.map do |preference| + # If `%{latest}` was specified then grab the most current version + pref = preference.gsub('%{latest}', '[0-9\\.]+$') + where_clause = safe_regexp_where_clause(column: 'identifier') + rslts = preference.include?('%{latest}') ? where(where_clause, pref) : where(identifier: pref) + rslts.order(:identifier).last + end + # Remove any preferred licenses that could not be found in the table + licenses.compact + } + + # varchar(255) NOT NULL + validates :name, + presence: { message: PRESENCE_MESSAGE }, + length: { in: 0..255, allow_nil: false } + + # varchar(255) NOT NULL + validates :identifier, + presence: { message: PRESENCE_MESSAGE }, + length: { in: 0..255, allow_nil: false } + + # varchar(255) NOT NULL + validates :uri, + presence: { message: PRESENCE_MESSAGE }, + length: { in: 0..255, allow_nil: false } +end diff --git a/app/models/metadata_standard.rb b/app/models/metadata_standard.rb new file mode 100644 index 0000000000..02eaccfd1f --- /dev/null +++ b/app/models/metadata_standard.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: metadata_standards +# +# id :bigint not null, primary key +# description :text +# locations :json +# related_entities :json +# title :string +# uri :string +# created_at :datetime not null +# updated_at :datetime not null +# rdamsc_id :string +# +class MetadataStandard < ApplicationRecord + # ============= + # = Constants = + # ============= + + # keep "=>" syntax as json_schemer requires string keys + SCHEMA_RELATED_ENTITIES = { + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'array', + 'items' => { + 'type' => 'object', + 'properties' => { + 'id' => { 'type' => 'string' }, + 'role' => { 'type' => 'string' } + } + } + }.freeze + + SCHEMA_LOCATIONS = { + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'array', + 'items' => { + 'type' => 'object', + 'properties' => { + 'url' => { 'type' => 'string' }, + 'type' => { 'type' => 'string' } + } + } + }.freeze + + # ================ + # = Associations = + # ================ + + has_and_belongs_to_many :research_outputs + + # ========== + # = Scopes = + # ========== + + scope :search, lambda { |term| + term = term.downcase + where('LOWER(title) LIKE ?', "%#{term}%").or(where('LOWER(description) LIKE ?', "%#{term}%")) + } + + # varchar(255) DEFAULT NULL + validates :title, + length: { maximum: 255 } + + # varchar(255) DEFAULT NULL + validates :rdamsc_id, + length: { maximum: 255 } + + # varchar(255) DEFAULT NULL + validates :uri, + length: { maximum: 255 } + + # json DEFAULT NULL + validates :related_entities, + json: { + schema: SCHEMA_RELATED_ENTITIES, + message: ->(errors) { errors } + }, + allow_nil: true + + # json DEFAULT NULL + validates :locations, + json: { + schema: SCHEMA_LOCATIONS, + message: ->(errors) { errors } + }, + allow_nil: true +end diff --git a/app/models/mime_type.rb b/app/models/mime_type.rb index 7c35a15b27..7cd2cda6c7 100644 --- a/app/models/mime_type.rb +++ b/app/models/mime_type.rb @@ -16,7 +16,6 @@ # index_mime_types_on_value (value) # class MimeType < ApplicationRecord - include ValidationMessages # ================ @@ -37,5 +36,4 @@ class MimeType < ApplicationRecord # Retrieves the unique list of categories scope :categories, -> { pluck(:category).uniq.sort { |a, b| a <=> b } } - end diff --git a/app/models/note.rb b/app/models/note.rb index 6e4b3cffd9..aeed0f2eaa 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -24,8 +24,8 @@ # fk_rails_... (user_id => users.id) # +# Object that represents a comment on a Plan class Note < ApplicationRecord - # ================ # = Associations = # ================ @@ -46,5 +46,4 @@ class Note < ApplicationRecord validates :archived, inclusion: { in: BOOLEAN_VALUES, message: INCLUSION_MESSAGE } - end diff --git a/app/models/notification.rb b/app/models/notification.rb index 4ec71198a9..b26be026f7 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -17,8 +17,8 @@ # updated_at :datetime not null # +# Object that represents a global notification class Notification < ApplicationRecord - enum level: %i[info warning danger] enum notification_type: %i[global] @@ -27,7 +27,7 @@ class Notification < ApplicationRecord # ================ has_and_belongs_to_many :users, dependent: :destroy, - join_table: "notification_acknowledgements" + join_table: 'notification_acknowledgements' # =============== # = Validations = @@ -56,7 +56,7 @@ class Notification < ApplicationRecord # ========== scope :active, (lambda do - where("starts_at <= :now and :now < expires_at", now: Time.now).where(enabled: true) + where('starts_at <= :now and :now < expires_at', now: Time.now).where(enabled: true) end) scope :active_per_user, (lambda do |user| @@ -75,5 +75,4 @@ class Notification < ApplicationRecord def acknowledged?(user) dismissable? && user.present? && users.include?(user) end - end diff --git a/app/models/org.rb b/app/models/org.rb index de2526de38..6b2a48525c 100644 --- a/app/models/org.rb +++ b/app/models/org.rb @@ -24,13 +24,15 @@ # language_id :integer # region_id :integer # managed :boolean default(false), not null +# helpdesk_email :string # # Foreign Keys # # fk_rails_... (language_id => languages.id) # -class Org < ApplicationRecord +# Object that represents an Organization/Institution/Funder +class Org < ApplicationRecord extend FeedbacksHelper include FlagShihTzu include Identifiable @@ -41,12 +43,12 @@ class Org < ApplicationRecord LOGO_FORMATS = %w[jpeg png gif jpg bmp].freeze HUMANIZED_ATTRIBUTES = { - feedback_msg: _("Feedback email message") + feedback_msg: _('Feedback email message') }.freeze attribute :feedback_msg, :text, default: feedback_confirmation_default_message attribute :language_id, :integer, default: -> { Language.default&.id } - attribute :links, :text, default: { "org": [] } + attribute :links, :text, default: { org: [] } # Stores links as an JSON object: # { org: [{"link":"www.example.com","text":"foo"}, ...] } @@ -70,7 +72,7 @@ class Org < ApplicationRecord has_many :plans - has_many :funded_plans, class_name: "Plan", foreign_key: "funder_id" + has_many :funded_plans, class_name: 'Plan', foreign_key: 'funder_id' has_many :templates @@ -81,7 +83,7 @@ class Org < ApplicationRecord has_many :annotations has_and_belongs_to_many :token_permission_types, - join_table: "org_token_permissions", + join_table: 'org_token_permissions', unique: true has_many :departments @@ -93,8 +95,6 @@ class Org < ApplicationRecord validates :name, presence: { message: PRESENCE_MESSAGE }, uniqueness: { message: UNIQUENESS_MESSAGE } - validates :abbreviation, presence: { message: PRESENCE_MESSAGE } - validates :is_other, inclusion: { in: BOOLEAN_VALUES, message: PRESENCE_MESSAGE } @@ -119,8 +119,8 @@ class Org < ApplicationRecord message: INCLUSION_MESSAGE } validates_property :format, of: :logo, in: LOGO_FORMATS, - message: _("must be one of the following formats: " \ - "jpeg, jpg, png, gif, bmp") + message: _('must be one of the following formats: ' \ + 'jpeg, jpg, png, gif, bmp') validates_size_of :logo, maximum: 500.kilobytes, @@ -155,11 +155,11 @@ def check_for_missing_logo_file # Attempt to locate the file by name. If it exists update the uid logo = Dir.glob("#{data_store_path}/**/*#{logo_name}") - if !logo.empty? - self.logo_uid = logo.first.gsub(data_store_path, "") - else + if logo.empty? # Otherwise the logo is missing so clear it to prevent save failures self.logo = nil + else + self.logo_uid = logo.first.gsub(data_store_path, '') end end @@ -172,7 +172,7 @@ def check_for_missing_logo_file 4 => :research_institute, 5 => :project, 6 => :school, - column: "org_type" + column: 'org_type' # The default Org is the one whose guidance is auto-attached to # plans when a plan is created @@ -188,16 +188,16 @@ def self.default_orgs scope :search, lambda { |term| search_pattern = "%#{term}%" - where("lower(orgs.name) LIKE lower(?) OR " \ - "lower(orgs.contact_email) LIKE lower(?)", + where('lower(orgs.name) LIKE lower(?) OR ' \ + 'lower(orgs.contact_email) LIKE lower(?)', search_pattern, search_pattern) } # Scope used in several controllers scope :with_template_and_user_counts, lambda { - joins("LEFT OUTER JOIN templates ON orgs.id = templates.org_id") - .joins("LEFT OUTER JOIN users ON orgs.id = users.org_id") - .group("orgs.id") + joins('LEFT OUTER JOIN templates ON orgs.id = templates.org_id') + .joins('LEFT OUTER JOIN users ON orgs.id = users.org_id') + .group('orgs.id') .select("orgs.*, count(distinct templates.family_id) as template_count, count(users.id) as user_count") @@ -232,17 +232,18 @@ def locale # Tests are setup currently to work with this issue. # # Returns String + # rubocop:disable Metrics/CyclomaticComplexity def org_type_to_s ret = [] - ret << "Institution" if institution? - ret << "Funder" if funder? - ret << "Organisation" if organisation? - ret << "Research Institute" if research_institute? - ret << "Project" if project? - ret << "School" if school? - (!ret.empty? ? ret.join(", ") : "None") + ret << 'Institution' if institution? + ret << 'Funder' if funder? + ret << 'Organisation' if organisation? + ret << 'Research Institute' if research_institute? + ret << 'Project' if project? + ret << 'School' if school? + (ret.empty? ? 'None' : ret.join(', ')) end - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity def funder_only? org_type == Org.org_type_values_for(:funder).min @@ -273,26 +274,30 @@ def short_name # # Returns ActiveRecord::Relation def published_templates - templates.where("published = ?", true) + templates.where('published = ?', true) end def org_admins admin_perms = %w[grant_permissions modify_templates modify_guidance change_org_details] - User.joins(:perms).where("users.org_id = ? AND perms.name IN (?)", id, admin_perms) + User.joins(:perms).where('users.org_id = ? AND perms.name IN (?)', id, admin_perms) end # This replaces the old plans method. We now use the native plans method and this. + # rubocop:disable Metrics/AbcSize def org_admin_plans combined_plan_ids = (native_plan_ids + affiliated_plan_ids).flatten.uniq if Rails.configuration.x.plans.org_admins_read_all Plan.includes(:template, :phases, :roles, :users).where(id: combined_plan_ids) + .where(roles: { active: true }) else Plan.includes(:template, :phases, :roles, :users).where(id: combined_plan_ids) .where.not(visibility: Plan.visibilities[:privately_visible]) .where.not(visibility: Plan.visibilities[:is_test]) + .where(roles: { active: true }) end end + # rubocop:enable Metrics/AbcSize def grant_api!(token_permission_type) token_permission_types << token_permission_type unless @@ -349,16 +354,16 @@ def merge!(to_be_merged:) def resize_image return if logo.nil? || logo.height == 100 - self.logo = logo.thumb("x100") # resize height and maintain aspect ratio + self.logo = logo.thumb('x100') # resize height and maintain aspect ratio end - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def merge_attributes!(to_be_merged:) return false unless to_be_merged.is_a?(Org) self.target_url = to_be_merged.target_url unless target_url.present? self.managed = true if !managed? && to_be_merged.managed? - self.links = to_be_merged.links unless links.nil? || links == "{\"org\":[]}" + self.links = to_be_merged.links unless links.nil? || links == '{"org":[]}' self.logo_uid = to_be_merged.logo_uid unless logo.present? self.logo_name = to_be_merged.logo_name unless logo.present? self.contact_email = to_be_merged.contact_email unless contact_email.present? @@ -366,8 +371,9 @@ def merge_attributes!(to_be_merged:) self.feedback_enabled = to_be_merged.feedback_enabled unless feedback_enabled? self.feedback_msg = to_be_merged.feedback_msg unless feedback_msg.present? end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # rubocop:disable Metrics/AbcSize def merge_departments!(to_be_merged:) return false unless to_be_merged.is_a?(Org) && to_be_merged.departments.any? @@ -378,6 +384,7 @@ def merge_departments!(to_be_merged:) department.update(org_id: id) unless existing.include?(department.name.downcase) end end + # rubocop:enable Metrics/AbcSize def merge_guidance_groups!(to_be_merged:) return false unless to_be_merged.is_a?(Org) && to_be_merged.guidance_groups.any? @@ -415,5 +422,4 @@ def ensure_api_access token_permission_types << perm unless token_permission_types.include?(perm) end end - end diff --git a/app/models/perm.rb b/app/models/perm.rb index 78462904ed..a95f6e6543 100644 --- a/app/models/perm.rb +++ b/app/models/perm.rb @@ -10,10 +10,9 @@ # updated_at :datetime not null # +# Object that represents a User permission class Perm < ApplicationRecord - class << self - private def lazy_load(name) @@ -23,7 +22,6 @@ def lazy_load(name) end .freeze end - end # ================ @@ -44,39 +42,38 @@ def lazy_load(name) # ================= def self.add_orgs - lazy_load("add_organisations") + lazy_load('add_organisations') end def self.change_affiliation - lazy_load("change_org_affiliation") + lazy_load('change_org_affiliation') end def self.grant_permissions - lazy_load("grant_permissions") + lazy_load('grant_permissions') end def self.modify_templates - lazy_load("modify_templates") + lazy_load('modify_templates') end def self.modify_guidance - lazy_load("modify_guidance") + lazy_load('modify_guidance') end def self.use_api - lazy_load("use_api") + lazy_load('use_api') end def self.change_org_details - lazy_load("change_org_details") + lazy_load('change_org_details') end def self.grant_api - lazy_load("grant_api_to_orgs") + lazy_load('grant_api_to_orgs') end def self.review_plans - lazy_load("review_org_plans") + lazy_load('review_org_plans') end - end diff --git a/app/models/phase.rb b/app/models/phase.rb index fcb01589e4..526115b18a 100644 --- a/app/models/phase.rb +++ b/app/models/phase.rb @@ -30,8 +30,9 @@ # # [+Created:+] 03/09/2014 # [+Copyright:+] Digital Curation Centre and University of California Curation Center -class Phase < ApplicationRecord +# Object that represents a Template phase +class Phase < ApplicationRecord include ActsAsSortable include VersionableModel include ConditionsHelper @@ -48,9 +49,9 @@ class Phase < ApplicationRecord belongs_to :plan, optional: true has_one :prefix_section, lambda { |phase| - modifiable.where("number < ?", + modifiable.where('number < ?', phase.sections.not_modifiable.minimum(:number)) - }, class_name: "Section" + }, class_name: 'Section' has_many :sections, dependent: :destroy @@ -62,7 +63,7 @@ class Phase < ApplicationRecord has_many :template_sections, lambda { not_modifiable - }, class_name: "Section" + }, class_name: 'Section' has_many :suffix_sections, lambda { |phase| modifiable.where(<<~SQL, phase_id: phase.id, modifiable: false) @@ -70,7 +71,7 @@ class Phase < ApplicationRecord WHERE sections.modifiable = :modifiable AND sections.phase_id = :phase_id) SQL - }, class_name: "Section" + }, class_name: 'Section' # =============== # = Validations = diff --git a/app/models/plan.rb b/app/models/plan.rb index 4809e82633..3a477c0a69 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -40,8 +40,9 @@ # fk_rails_... (research_domain_id => research_domains.id) # +# Object that represents an DMP +# rubocop:disable Metrics/ClassLength class Plan < ApplicationRecord - include ConditionalUserMailer include ExportablePlan include DateRangeable @@ -54,19 +55,19 @@ class Plan < ApplicationRecord # Returns visibility message given a Symbol type visibility passed, otherwise # nil VISIBILITY_MESSAGE = { - organisationally_visible: _("organisational"), - publicly_visible: _("public"), - is_test: _("test"), - privately_visible: _("private") + organisationally_visible: _('organisational'), + publicly_visible: _('public'), + is_test: _('test'), + privately_visible: _('private') }.freeze - VISIBILITY_ORDER = %i[privately_visible publicly_visible organisationally_visible - is_test] + VISIBILITY_ORDER = %i[privately_visible publicly_visible organisationally_visible + is_test].freeze FUNDING_STATUS = { - planned: _("Planned"), - funded: _("Funded"), - denied: _("Denied") + planned: _('Planned'), + funded: _('Funded'), + denied: _('Denied') }.freeze # ============== @@ -91,7 +92,7 @@ class Plan < ApplicationRecord belongs_to :org - belongs_to :funder, class_name: "Org", optional: true + belongs_to :funder, class_name: 'Org', optional: true belongs_to :research_domain, optional: true @@ -105,10 +106,10 @@ class Plan < ApplicationRecord has_many :guidances, through: :themes - has_many :guidance_group_options, -> { distinct.published.reorder("id") }, + has_many :guidance_group_options, -> { distinct.published.reorder('id') }, through: :guidances, source: :guidance_group, - class_name: "GuidanceGroup" + class_name: 'GuidanceGroup' has_many :answers, dependent: :destroy @@ -120,10 +121,14 @@ class Plan < ApplicationRecord has_and_belongs_to_many :guidance_groups, join_table: :plans_guidance_groups - has_many :exported_plans + has_many :exported_plans, dependent: :destroy has_many :contributors, dependent: :destroy + has_one :grant, as: :identifiable, dependent: :destroy, class_name: 'Identifier' + + has_many :research_outputs, dependent: :destroy + # ===================== # = Nested Attributes = # ===================== @@ -170,7 +175,7 @@ class Plan < ApplicationRecord visibilities[:publicly_visible] ]) .where( - "NOT EXISTS (SELECT 1 FROM roles WHERE plan_id = plans.id AND user_id = ?)", + 'NOT EXISTS (SELECT 1 FROM roles WHERE plan_id = plans.id AND user_id = ?)', user.id ) } @@ -207,7 +212,7 @@ class Plan < ApplicationRecord ## # Settings for the template - has_settings :export, class_name: "Settings::Template" do |s| + has_settings :export, class_name: 'Settings::Template' do |s| s.key :export, defaults: Settings::Template::DEFAULT_SETTINGS end alias super_settings settings @@ -243,9 +248,10 @@ def self.load_for_phase(plan_id, phase_id) # plan - Plan to be deep copied # # Returns Plan + # rubocop:disable Metrics/AbcSize def self.deep_copy(plan) plan_copy = plan.dup - plan_copy.title = format(_("Copy of %{title}"), title: plan.title) + plan_copy.title = "Copy of #{plan.title}" plan_copy.feedback_requested = false plan_copy.save! plan.answers.each do |answer| @@ -258,6 +264,7 @@ def self.deep_copy(plan) end plan_copy end + # rubocop:enable Metrics/AbcSize # =========================== # = Public instance methods = @@ -287,6 +294,7 @@ def settings(key) # # Returns Answer # Returns nil + # rubocop:disable Metrics/AbcSize, Style/OptionalBooleanParameter def answer(qid, create_if_missing = true) answer = answers.select { |a| a.question_id == qid } .max { |a, b| a.created_at <=> b.created_at } @@ -304,6 +312,7 @@ def answer(qid, create_if_missing = true) end answer end + # rubocop:enable Metrics/AbcSize, Style/OptionalBooleanParameter alias get_guidance_group_options guidance_group_options @@ -344,7 +353,7 @@ def complete_feedback(org_admin) # Send an email confirmation to the owners and co-owners deliver_if(recipients: owner_and_coowners, - key: "users.feedback_provided") do |r| + key: 'users.feedback_provided') do |r| UserMailer.feedback_complete( r, self, @@ -374,6 +383,7 @@ def editable_by?(user_id) # user_id - The Integer id for a user # # Returns Boolean + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def readable_by?(user_id) return true if commentable_by?(user_id) @@ -390,7 +400,7 @@ def readable_by?(user_id) false end end - # rubocop:enable + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # determines if the plan is readable by the specified user. # @@ -438,7 +448,7 @@ def latest_update def owner r = roles.select { |rr| rr.active && rr.administrator } .min { |a, b| a.created_at <=> b.created_at } - r.nil? ? nil : r.user + r&.user end # Creates a role for the specified user (will update the user's @@ -535,7 +545,7 @@ def visibility_allowed? # # Returns Boolean def question_exists?(question_id) - Plan.joins(:questions).exists?(id: id, "questions.id": question_id) + Plan.joins(:questions).exists?(id: id, 'questions.id': question_id) end # Determines what percentage of the Plan's questions have been num_answered_questions @@ -584,6 +594,7 @@ def grant # Helper method to convert the grant id value entered by the user into an Identifier # works with both controller params or an instance of Identifier + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity def grant=(params) val = params.present? ? params[:value] : nil current = grant @@ -599,6 +610,7 @@ def grant=(params) current = Identifier.create(identifiable: self, value: val) self.grant_id = current.id end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity private @@ -607,7 +619,7 @@ def end_date_after_start_date # allow nil values return true if end_date.blank? || start_date.blank? - errors.add(:end_date, _("must be after the start date")) if end_date < start_date + errors.add(:end_date, _('must be after the start date')) if end_date < start_date end - end +# rubocop:enable Metrics/ClassLength diff --git a/app/models/pref.rb b/app/models/pref.rb index 7edd20b938..01f83ee179 100644 --- a/app/models/pref.rb +++ b/app/models/pref.rb @@ -9,8 +9,8 @@ # user_id :integer # +# Object that represents a User's email preferences class Pref < ApplicationRecord - ## # Serialize prefs to JSON serialize :settings, JSON @@ -34,5 +34,4 @@ class Pref < ApplicationRecord def self.default_settings Rails.configuration.x.application.preferences end - end diff --git a/app/models/question.rb b/app/models/question.rb index e8504cfdda..637ae0170a 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -27,8 +27,9 @@ # fk_rails_... (question_format_id => question_formats.id) # fk_rails_... (section_id => sections.id) # -class Question < ApplicationRecord +# Object that represents a Template question +class Question < ApplicationRecord include ActsAsSortable include VersionableModel @@ -54,7 +55,7 @@ class Question < ApplicationRecord has_many :annotations, dependent: :destroy, inverse_of: :question - has_and_belongs_to_many :themes, join_table: "questions_themes" + has_and_belongs_to_many :themes, join_table: 'questions_themes' belongs_to :section @@ -101,11 +102,8 @@ class Question < ApplicationRecord accepts_nested_attributes_for :question_options, allow_destroy: true, reject_if: ->(a) { a[:text].blank? } - # rubocop:disable Layout/LineLength accepts_nested_attributes_for :annotations, allow_destroy: true, reject_if: proc { |a| a[:text].blank? && a[:id].blank? } - # rubocop:enable Layout/LineLength - # ===================== # = Delegated methods = # ===================== @@ -126,7 +124,8 @@ def default_value default_value = read_attribute(:default_value) _(default_value) unless default_value.blank? end - + + # rubocop:disable Metrics/AbcSize def deep_copy(**options) copy = dup copy.modifiable = options.fetch(:modifiable, modifiable) @@ -152,6 +151,7 @@ def deep_copy(**options) # org - The Org to find guidance for # # Returns Hash + # rubocop:disable Metrics/AbcSize def guidance_for_org(org) # pulls together guidance from various sources for question guidances = {} @@ -160,9 +160,7 @@ def guidance_for_org(org) .where(org_id: org.id).each do |group| group.guidances.each do |g| g.themes.each do |theme| - if theme_ids.include? theme.id - guidances["#{group.name} " + _("guidance on") + " #{theme.title}"] = g - end + guidances["#{group.name} " + _('guidance on') + " #{theme.title}"] = g if theme_ids.include? theme.id end end end @@ -170,6 +168,7 @@ def guidance_for_org(org) guidances end + # rubocop:enable Metrics/AbcSize # get example answer belonging to the currents user for this question # @@ -210,10 +209,8 @@ def annotations_per_org(org_id) type: Annotation.types[:example_answer]) guidance = annotations.find_by(org_id: org_id, type: Annotation.types[:guidance]) - unless example_answer.present? - example_answer = annotations.build(type: :example_answer, text: "", org_id: org_id) - end - guidance = annotations.build(type: :guidance, text: "", org_id: org_id) unless guidance.present? + example_answer = annotations.build(type: :example_answer, text: '', org_id: org_id) unless example_answer.present? + guidance = annotations.build(type: :guidance, text: '', org_id: org_id) unless guidance.present? [example_answer, guidance] end @@ -232,10 +229,10 @@ def update_conditions(param_conditions, old_to_new_opts, question_id_map) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize def save_condition(value, opt_map, question_id_map) c = conditions.build - c.action_type = value["action_type"] - c.number = value["number"] + c.action_type = value['action_type'] + c.number = value['number'] # question options may have changed so rewrite them - c.option_list = value["question_option"] + c.option_list = value['question_option'] unless opt_map.blank? new_question_options = [] c.option_list.each do |qopt| @@ -244,8 +241,8 @@ def save_condition(value, opt_map, question_id_map) c.option_list = new_question_options end - if value["action_type"] == "remove" - c.remove_data = value["remove_question_id"] + if value['action_type'] == 'remove' + c.remove_data = value['remove_question_id'] unless question_id_map.blank? new_question_ids = [] c.remove_data.each do |qid| @@ -255,10 +252,10 @@ def save_condition(value, opt_map, question_id_map) end else c.webhook_data = { - name: value["webhook-name"], - email: value["webhook-email"], - subject: value["webhook-subject"], - message: value["webhook-message"] + name: value['webhook-name'], + email: value['webhook-email'], + subject: value['webhook-subject'], + message: value['webhook-message'] }.to_json end c.save @@ -275,6 +272,7 @@ def ensure_has_question_options # and condition's remove_data and also if that remove_data is empty # destroy the condition. # abort callback chain if we can't update the condition + # rubocop:disable Metrics/AbcSize def check_remove_conditions id = self.id.to_s template.questions.each do |q| @@ -288,5 +286,5 @@ def check_remove_conditions end end end - + # rubocop:enable Metrics/AbcSize end diff --git a/app/models/question_format.rb b/app/models/question_format.rb index 259b992e7a..1bcc3d3faf 100644 --- a/app/models/question_format.rb +++ b/app/models/question_format.rb @@ -13,8 +13,8 @@ # updated_at :datetime not null # +# Object that represents a question type class QuestionFormat < ApplicationRecord - ## # FORMAT_TYPES = %i[textarea textfield radiobuttons checkbox dropdown @@ -46,7 +46,7 @@ class QuestionFormat < ApplicationRecord validates :description, presence: { message: PRESENCE_MESSAGE } validates :option_based, inclusion: { in: BOOLEAN_VALUES } - + # =========================== # = Public instance methods = # =========================== @@ -70,5 +70,4 @@ def description def self.id_for(formattype) where(formattype: formattype).pluck(:id).first end - end diff --git a/app/models/question_option.rb b/app/models/question_option.rb index cc0787723e..9de8fb6451 100644 --- a/app/models/question_option.rb +++ b/app/models/question_option.rb @@ -21,8 +21,8 @@ # fk_rails_... (question_id => questions.id) # +# Object that represents an option for a multi-select question class QuestionOption < ApplicationRecord - include VersionableModel # ================ @@ -95,5 +95,4 @@ def check_condition_options cond.destroy if cond.option_list.include?(id) end end - end diff --git a/app/models/region.rb b/app/models/region.rb index 3c970f05d2..e091e3721b 100644 --- a/app/models/region.rb +++ b/app/models/region.rb @@ -12,15 +12,15 @@ # updated_at :datetime not null # +# Object that represents a regional area class Region < ApplicationRecord - # ================ # = Associations = # ================ - has_many :sub_regions, class_name: "Region", foreign_key: "super_region_id" + has_many :sub_regions, class_name: 'Region', foreign_key: 'super_region_id' - belongs_to :super_region, class_name: "Region", optional: true + belongs_to :super_region, class_name: 'Region', optional: true # =============== # = Validations = @@ -33,5 +33,4 @@ class Region < ApplicationRecord validates :abbreviation, presence: { message: PRESENCE_MESSAGE }, uniqueness: { message: UNIQUENESS_MESSAGE } - end diff --git a/app/models/repository.rb b/app/models/repository.rb new file mode 100644 index 0000000000..344dcb889b --- /dev/null +++ b/app/models/repository.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: repositories +# +# id :bigint not null, primary key +# contact :string +# description :text not null +# info :json +# name :string not null +# homepage :string +# created_at :datetime not null +# updated_at :datetime not null +# uri :string not null +# +# Indexes +# +# index_repositories_on_name (name) +# index_repositories_on_homepage (homepage) +# index_repositories_on_uri (uri) +# +# Object that represents a research output repository (e.g. GitHub or Zenodo) +class Repository < ApplicationRecord + # ============= + # = Constants = + # ============= + + # keep "=>" syntax as json_schemer requires string keys + SCHEMA_INFO = { + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'object', + 'properties' => { + 'types' => { + 'type' => 'array', + 'items' => { + 'type' => 'string' + } + }, + 'keywords' => { + 'type' => 'array', + 'items' => { + 'type' => 'string' + } + }, + 'subjects' => { + 'type' => 'array', + 'items' => { + 'type' => 'string' + } + }, + 'access' => { + 'type' => 'string', + 'enum' => %w[open restricted closed] + }, + 'provider_types' => { + 'type' => 'array', + 'items' => { + 'type' => 'string' + } + }, + 'upload_types' => { + 'type' => 'array', + 'items' => { + 'type' => 'object', + 'properties' => { + 'type' => { 'type' => 'string' }, + 'restriction' => { 'type' => 'string' } + }, + 'required' => %w[type restriction] + } + }, + 'policies' => { + 'type' => 'array', + 'items' => { + 'type' => 'object', + 'properties' => { + 'name' => { 'type' => 'string' }, + 'url' => { 'type' => 'string' } + }, + 'required' => %w[name url] + } + }, + 'pid_system' => { + 'type' => 'string' + } + } + }.freeze + + # ================ + # = Associations = + # ================ + + has_and_belongs_to_many :research_outputs + + # ========== + # = Scopes = + # ========== + + scope :by_type, lambda { |type| + where(safe_json_where_clause(column: 'info', hash_key: 'types'), "%#{type}%") + } + + scope :by_subject, lambda { |subject| + where(safe_json_where_clause(column: 'info', hash_key: 'subjects'), "%#{subject}%") + } + + scope :search, lambda { |term| + term = term.downcase + where('LOWER(name) LIKE ?', "%#{term}%") + .or(where(safe_json_where_clause(column: 'info', hash_key: 'keywords'), "%#{term}%")) + .or(where(safe_json_where_clause(column: 'info', hash_key: 'subjects'), "%#{term}%")) + } + + # A very specific keyword search (e.g. 'gene', 'DNA', etc.) + scope :by_facet, lambda { |facet| + where(safe_json_where_clause(column: 'info', hash_key: 'keywords'), "%#{facet}%") + } + + # =============== + # = Validations = + # =============== + + # varchar(255) NOT NULL + validates :name, + presence: { message: PRESENCE_MESSAGE }, + length: { in: 0..255, allow_nil: false } + + # text NOT NULL + validates :description, + presence: { message: PRESENCE_MESSAGE } + + # varchar(255) NOT NULL + validates :uri, + presence: { message: PRESENCE_MESSAGE }, + length: { in: 0..255, allow_nil: false } + + # varchar(255) DEFAULT NULL + validates :homepage, + length: { maximum: 255 } + + # varchar(255) DEFAULT NULL + validates :contact, + length: { maximum: 255 } + + # json DEFAULT NULL + validates :info, + json: { + schema: SCHEMA_INFO, + message: ->(errors) { errors } + }, + allow_nil: true +end diff --git a/app/models/research_domain.rb b/app/models/research_domain.rb index be049807c9..87f8abd4a6 100644 --- a/app/models/research_domain.rb +++ b/app/models/research_domain.rb @@ -19,14 +19,14 @@ # # fk_rails_... (parent_id => research_domains.id) # -class ResearchDomain < ApplicationRecord +# Object that represents a disciple (e.g. Physics, Social Sciences, etc.) +class ResearchDomain < ApplicationRecord # ================ # = Associations = # ================ # Self join - has_many :sub_fields, class_name: "ResearchDomain", foreign_key: "parent_id" - belongs_to :parent, class_name: "ResearchDomain", optional: true - + has_many :sub_fields, class_name: 'ResearchDomain', foreign_key: 'parent_id' + belongs_to :parent, class_name: 'ResearchDomain', optional: true end diff --git a/app/models/research_output.rb b/app/models/research_output.rb index 858ef06d10..9ecfdf49fe 100644 --- a/app/models/research_output.rb +++ b/app/models/research_output.rb @@ -6,12 +6,12 @@ # # id :bigint not null, primary key # abbreviation :string -# access :integer default(0), not null +# access :integer default("open"), not null # byte_size :bigint # description :text # display_order :integer -# is_default :boolean default("false") -# output_type :integer default(3), not null +# is_default :boolean +# output_type :integer default("dataset"), not null # output_type_description :string # personal_data :boolean # release_date :datetime @@ -19,16 +19,21 @@ # title :string not null # created_at :datetime not null # updated_at :datetime not null -# mime_type_id :integer +# license_id :bigint # plan_id :integer # # Indexes # # index_research_outputs_on_output_type (output_type) -# index_research_outputs_on_plan_id (plan_id) # -class ResearchOutput < ApplicationRecord +# Foreign Keys +# +# fk_rails_... (plan_id => plans.id) +# fk_rails_... (license_id => licenses.id) +# +# Object that represents a proposed output for a project +class ResearchOutput < ApplicationRecord include Identifiable include ValidationMessages @@ -42,14 +47,22 @@ class ResearchOutput < ApplicationRecord # = Associations = # ================ - belongs_to :plan, optional: true + belongs_to :plan, optional: true, touch: true + belongs_to :license, optional: true + + has_and_belongs_to_many :metadata_standards + has_and_belongs_to_many :repositories # =============== # = Validations = # =============== validates_presence_of :output_type, :access, :title, message: PRESENCE_MESSAGE - validates_uniqueness_of :title, :abbreviation, scope: :plan_id + validates_uniqueness_of :title, { case_sensitive: false, scope: :plan_id, + message: UNIQUENESS_MESSAGE } + validates_uniqueness_of :abbreviation, { case_sensitive: false, scope: :plan_id, + allow_nil: true, allow_blank: true, + message: UNIQUENESS_MESSAGE } # Ensure presence of the :output_type_description if the user selected 'other' validates_presence_of :output_type_description, if: -> { other? }, message: PRESENCE_MESSAGE @@ -58,37 +71,17 @@ class ResearchOutput < ApplicationRecord # = Instance methods = # ==================== - # TODO: placeholders for once the License, Repository, Metadata Standard and - # Resource Type Lookups feature is built. - # - # Be sure to add the scheme in the appropriate upgrade task (and to the - # seed.rb as well) - def licenses - # scheme = IdentifierScheme.find_by(name: '[name of license scheme]') - # return [] unless scheme.present? - # identifiers.select { |id| id.identifier_scheme = scheme } - [] + # Helper method to convert selected repository form params into Repository objects + def repositories_attributes=(params) + params.each do |_i, repository_params| + repositories << Repository.find_by(id: repository_params[:id]) + end end - def repositories - # scheme = IdentifierScheme.find_by(name: '[name of repository scheme]') - # return [] unless scheme.present? - # identifiers.select { |id| id.identifier_scheme = scheme } - [] + # Helper method to convert selected metadata standard form params into MetadataStandard objects + def metadata_standards_attributes=(params) + params.each do |_i, metadata_standard_params| + metadata_standards << MetadataStandard.find_by(id: metadata_standard_params[:id]) + end end - - def metadata_standards - # scheme = IdentifierScheme.find_by(name: '[name of openaire scheme]') - # return [] unless scheme.present? - # identifiers.select { |id| id.identifier_scheme = scheme } - [] - end - - def resource_types - # scheme = IdentifierScheme.find_by(name: '[name of resource_type scheme]') - # return [] unless scheme.present? - # identifiers.select { |id| id.identifier_scheme = scheme } - [] - end - end diff --git a/app/models/research_project.rb b/app/models/research_project.rb index 68f3aea319..9786171dfe 100644 --- a/app/models/research_project.rb +++ b/app/models/research_project.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Object that represents a grant ResearchProject = Struct.new(:grant_id, :description) do def to_json(_val = nil) { grant_id: grant_id, description: description }.to_json diff --git a/app/models/role.rb b/app/models/role.rb index c42755c671..d6df1e10e0 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -23,8 +23,8 @@ # fk_rails_... (user_id => users.id) # +# Object that represents a User's relationship to a Plan class Role < ApplicationRecord - include FlagShihTzu attribute :active, :boolean, default: true @@ -45,7 +45,7 @@ class Role < ApplicationRecord 3 => :editor, # 4 4 => :commenter, # 8 5 => :reviewer, # 16 - column: "access" + column: 'access' # =============== # = Validations = @@ -69,10 +69,10 @@ class Role < ApplicationRecord # # Return ActiveRecord::Relation scope :with_access_flags, lambda { |*flags| - bad_flag = flags.detect { |flag| !flag.in?(flag_mapping["access"].keys) } + bad_flag = flags.detect { |flag| !flag.in?(flag_mapping['access'].keys) } raise ArgumentError, "Unkown access flag '#{bad_flag}'" if bad_flag - access_values = flags.map { |flag| sql_in_for_flag(flag.to_sym, "access") } + access_values = flags.map { |flag| sql_in_for_flag(flag.to_sym, 'access') } .flatten .uniq where(access: access_values) @@ -91,7 +91,7 @@ class Role < ApplicationRecord # # Returns [Integer] def self.bit_values(access) - Role.send(:chained_flags_values, "access", access) + Role.send(:chained_flags_values, 'access', access) end # =========================== @@ -113,7 +113,6 @@ def deactivate! false end end - end # ----------------------------------------------------- diff --git a/app/models/section.rb b/app/models/section.rb index 9779e19aee..48e91788a3 100644 --- a/app/models/section.rb +++ b/app/models/section.rb @@ -24,8 +24,8 @@ # fk_rails_... (phase_id => phases.id) # +# Object that represents a Template section class Section < ApplicationRecord - include ActsAsSortable include VersionableModel @@ -143,5 +143,4 @@ def deep_copy(**options) def unmodifiable? !modifiable? end - end diff --git a/app/models/section_sorter.rb b/app/models/section_sorter.rb index 385f359635..cedf536ccd 100644 --- a/app/models/section_sorter.rb +++ b/app/models/section_sorter.rb @@ -8,8 +8,9 @@ # SectionSorter.new(*@phase.sections).sort! # => Array of sorted Sections # # -class SectionSorter +# Object allows a user to sort sections of a Template via drag-drop +class SectionSorter ## # Access the array of Sections # @@ -29,10 +30,9 @@ def initialize(*sections) # # Returns Array of Sections # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def sort! - if all_sections_unmodifiable? - sort_as_homogenous_group - elsif all_sections_modifiable? + if all_sections_unmodifiable? || all_sections_modifiable? sort_as_homogenous_group else # If there are duplicates in the #1 position @@ -73,7 +73,7 @@ def sort! end end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity private @@ -106,5 +106,4 @@ def all_sections_modifiable? def sort_as_homogenous_group sections.sort_by { |section| [section.number, section.id] } end - end diff --git a/app/models/settings/template.rb b/app/models/settings/template.rb index 2a05c58b64..a0ee4c6377 100644 --- a/app/models/settings/template.rb +++ b/app/models/settings/template.rb @@ -14,12 +14,11 @@ # module Settings - + # Records export settings for a Plan and defaults for the template class Template < RailsSettings::SettingObject - VALID_FONT_FACES = [ '"Times New Roman", Times, Serif', - "Arial, Helvetica, Sans-Serif" + 'Arial, Helvetica, Sans-Serif' ].freeze VALID_FONT_SIZE_RANGE = (8..14).freeze @@ -39,7 +38,7 @@ class Template < RailsSettings::SettingObject left: 12, right: 12 }, - font_face: "Arial, Helvetica, Sans-Serif", + font_face: 'Arial, Helvetica, Sans-Serif', font_size: 10 # pt }, max_pages: 3, @@ -47,13 +46,13 @@ class Template < RailsSettings::SettingObject admin: VALID_ADMIN_FIELDS, questions: :all }, - title: "" + title: '' }.freeze # rubocop:disable Metrics/BlockLength, Metrics/BlockNesting validate do - formatting = value["formatting"] - max_pages = value["max_pages"] + formatting = value['formatting'] + max_pages = value['max_pages'] if formatting.present? errs = [] @@ -62,12 +61,8 @@ class Template < RailsSettings::SettingObject if (default_formatting.keys - formatting.keys).empty? if formatting[:margin].is_a?(Hash) errs << :negative_margin if formatting[:margin].any? { |_k, v| v.to_i.negative? } - unless (formatting[:margin].keys - default_formatting[:margin].keys).empty? - errs << :unknown_margin - end - unless formatting[:margin].all? { |_k, v| VALID_MARGIN_RANGE.member?(v) } - errs << :invalid_margin - end + errs << :unknown_margin unless (formatting[:margin].keys - default_formatting[:margin].keys).empty? + errs << :invalid_margin unless formatting[:margin].all? { |_k, v| VALID_MARGIN_RANGE.member?(v) } else errs << :invalid_margin end @@ -80,36 +75,35 @@ class Template < RailsSettings::SettingObject end errs.map do |key| - if key == :missing_key - errors.add(:formatting, _("A required setting has not been provided")) - elsif key == :invalid_margin - errors.add(:formatting, _("Margin value is invalid")) - elsif key == :negative_margin - errors.add(:formatting, _("Margin cannot be negative")) - elsif key == :unknown_margin - # rubocop:disable Layout/LineLength + case key + when :missing_key + errors.add(:formatting, _('A required setting has not been provided')) + when :invalid_margin + errors.add(:formatting, _('Margin value is invalid')) + when :negative_margin + errors.add(:formatting, _('Margin cannot be negative')) + when :unknown_margin errors.add(:formatting, _("Unknown margin. Can only be 'top', 'bottom', 'left' or 'right'")) - # rubocop:enable Layout/LineLength - elsif key == :invalid_font_size - errors.add(:formatting, _("Invalid font size")) - elsif key == :invalid_font_face - errors.add(:formatting, _("Invalid font face")) - elsif key == :unknown_key - errors.add(:formatting, _("Unknown formatting setting")) + when :invalid_font_size + errors.add(:formatting, _('Invalid font size')) + when :invalid_font_face + errors.add(:formatting, _('Invalid font face')) + when :unknown_key + errors.add(:formatting, _('Unknown formatting setting')) end end end if max_pages.present? && (!max_pages.is_a?(Integer) || max_pages <= 0) - errors.add(:max_pages, _("Invalid maximum pages")) + errors.add(:max_pages, _('Invalid maximum pages')) end end # rubocop:enable Metrics/BlockLength, Metrics/BlockNesting before_validation do formatting[:font_size] = formatting[:font_size].to_i if formatting[:font_size].present? - unless formatting[:margin].nil? or !formatting[:margin].is_a?(Hash) + unless formatting[:margin].nil? || !formatting[:margin].is_a?(Hash) formatting[:margin].each do |key, val| formatting[:margin][key] = val.to_i end @@ -127,7 +121,5 @@ class Template < RailsSettings::SettingObject fields[:admin] ||= [] fields[:questions] ||= [] end - end - end diff --git a/app/models/stat.rb b/app/models/stat.rb index 5f16bae257..7eac950684 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -15,8 +15,8 @@ # org_id :integer # +# Object that represents a generic usage statistic class Stat < ApplicationRecord - extend OrgDateRangeable belongs_to :org, optional: true @@ -24,18 +24,15 @@ class Stat < ApplicationRecord validates_uniqueness_of :type, scope: %i[date org_id filtered] class << self - - def to_csv(stats, sep = ",") + def to_csv(stats, sep = ',') data = stats.map do |stat| { date: stat.date, count: stat.count } end Csvable.from_array_of_hashes(data, sep) end - end def to_json(methods: nil) super(only: %i[count date], methods: methods) end - end diff --git a/app/models/stat_created_plan.rb b/app/models/stat_created_plan.rb index 0bd760bdbe..6ba26d2271 100644 --- a/app/models/stat_created_plan.rb +++ b/app/models/stat_created_plan.rb @@ -15,18 +15,18 @@ # org_id :integer # -require "set" +require 'set' +# Object that represents a Nbr of Plans created usage statistic class StatCreatedPlan < Stat - serialize :details, JSON def by_template - parse_details.fetch("by_template", []) + parse_details.fetch('by_template', []) end def using_template - parse_details.fetch("using_template", []) + parse_details.fetch('using_template', []) end def to_json(_options = nil) @@ -40,8 +40,7 @@ def parse_details end class << self - - def to_csv(created_plans, details: { by_template: false, sep: "," }) + def to_csv(created_plans, details: { by_template: false, sep: ',' }) if details[:by_template] to_csv_by_template(created_plans, details[:sep]) else @@ -51,31 +50,31 @@ def to_csv(created_plans, details: { by_template: false, sep: "," }) private - def to_csv_by_template(created_plans, sep = ",") + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity + def to_csv_by_template(created_plans, sep = ',') template_names = lambda do |plns| unique = Set.new plns.each do |created_plan| created_plan.by_template&.each do |name_count| - unique.add(name_count.fetch("name")) + unique.add(name_count.fetch('name')) end end unique.to_a end.call(created_plans) data = created_plans.map do |created_plan| - tuple = { Date: created_plan.date.strftime("%b %Y") } + tuple = { Date: created_plan.date.strftime('%b %Y') } template_names.each_with_object(tuple) do |name, acc| acc[name] = 0 end created_plan.by_template&.each do |name_count| - tuple[name_count.fetch("name")] = name_count.fetch("count") + tuple[name_count.fetch('name')] = name_count.fetch('count') end tuple[:Count] = created_plan.count tuple end Csvable.from_array_of_hashes(data, false, sep) end - + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity end - end diff --git a/app/models/stat_created_plan/create_or_update.rb b/app/models/stat_created_plan/create_or_update.rb index 426ad07f1f..182b510141 100644 --- a/app/models/stat_created_plan/create_or_update.rb +++ b/app/models/stat_created_plan/create_or_update.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true +# Usage Stats class StatCreatedPlan - + # Usage statistics helper class CreateOrUpdate - class << self - + # rubocop:disable Metrics/MethodLength def do(start_date:, end_date:, org:, filtered: false) count = count_plans(start_date: start_date, end_date: end_date, org: org, filtered: filtered) @@ -32,6 +32,7 @@ def do(start_date:, end_date:, org:, filtered: false) StatCreatedPlan.create(attrs) end end + # rubocop:enable Metrics/MethodLength private @@ -73,9 +74,9 @@ def plan_statistics(start_date:, end_date:, org:, filtered:, own_templates: fals roleable_plan_ids = roleable_plans.pluck(:plan_id).uniq template_counts = Plan.joins(:template).where(id: roleable_plan_ids) - .group("templates.family_id").count + .group('templates.family_id').count most_recent_versions = Template.where(family_id: template_counts.keys) - .group(:family_id).maximum("version") + .group(:family_id).maximum('version') most_recent_versions = most_recent_versions.map { |k, v| "#{k}=#{v}" } template_names = Template.where("CONCAT(family_id, '=', version) IN (?)", most_recent_versions).pluck(:family_id, :title) @@ -84,9 +85,6 @@ def plan_statistics(start_date:, end_date:, org:, filtered:, own_templates: fals end end # rubocop:enable Metrics/AbcSize - end - end - end diff --git a/app/models/stat_exported_plan.rb b/app/models/stat_exported_plan.rb index 2f9e518cf8..0e56ed19e7 100644 --- a/app/models/stat_exported_plan.rb +++ b/app/models/stat_exported_plan.rb @@ -15,14 +15,11 @@ # org_id :integer # +# Object that represents a Nbr of Plans exported usage statistic class StatExportedPlan < Stat - class << self - def to_csv(exported_plans) Stat.to_csv(exported_plans) end - end - end diff --git a/app/models/stat_exported_plan/create_or_update.rb b/app/models/stat_exported_plan/create_or_update.rb index 6e045905cb..0ef504976c 100644 --- a/app/models/stat_exported_plan/create_or_update.rb +++ b/app/models/stat_exported_plan/create_or_update.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true +# Usage Stats class StatExportedPlan - + # Usage statistics helper class CreateOrUpdate - class << self - def do(start_date:, end_date:, org:, filtered: false) count = exported_plans(start_date: start_date, end_date: end_date, org_id: org.id, filtered: filtered) @@ -46,9 +45,6 @@ def exported_plans(start_date:, end_date:, org_id:, filtered:) .where(created_at: start_date..end_date) .count end - end - end - end diff --git a/app/models/stat_joined_user.rb b/app/models/stat_joined_user.rb index 2910e8b476..5659b5ed16 100644 --- a/app/models/stat_joined_user.rb +++ b/app/models/stat_joined_user.rb @@ -15,14 +15,11 @@ # org_id :integer # +# Object that represents a Nbr of Users created usage statistic class StatJoinedUser < Stat - class << self - def to_csv(joined_users) Stat.to_csv(joined_users) end - end - end diff --git a/app/models/stat_joined_user/create_or_update.rb b/app/models/stat_joined_user/create_or_update.rb index 977a469369..7481e847aa 100644 --- a/app/models/stat_joined_user/create_or_update.rb +++ b/app/models/stat_joined_user/create_or_update.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true +# Usage Stats class StatJoinedUser - + # Usage statistics helper class CreateOrUpdate - class << self - def do(start_date:, end_date:, org:, filtered: false) count = count_users(start_date: start_date, end_date: end_date, org_id: org.id) attrs = { date: end_date.to_date, count: count, org_id: org.id, filtered: filtered } @@ -28,9 +27,6 @@ def do(start_date:, end_date:, org:, filtered: false) def count_users(start_date:, end_date:, org_id:) User.where(created_at: start_date..end_date, org_id: org_id).count end - end - end - end diff --git a/app/models/stat_shared_plan.rb b/app/models/stat_shared_plan.rb index 96230bab20..75ab6981f5 100644 --- a/app/models/stat_shared_plan.rb +++ b/app/models/stat_shared_plan.rb @@ -15,14 +15,11 @@ # org_id :integer # +# Object that represents a Nbr of Plans shared usage statistic class StatSharedPlan < Stat - class << self - def to_csv(shared_plans) Stat.to_csv(shared_plans) end - end - end diff --git a/app/models/stat_shared_plan/create_or_update.rb b/app/models/stat_shared_plan/create_or_update.rb index 26dd326fb7..ba3ffe1ddd 100644 --- a/app/models/stat_shared_plan/create_or_update.rb +++ b/app/models/stat_shared_plan/create_or_update.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true +# Usage Stats class StatSharedPlan - + # Usage statistics helper class CreateOrUpdate - class << self - def do(start_date:, end_date:, org:, filtered: false) count = shared_plans(start_date: start_date, end_date: end_date, org_id: org.id, filtered: filtered) @@ -47,9 +46,6 @@ def shared_plans(start_date:, end_date:, org_id:, filtered:) .where(created_at: start_date..end_date) .count end - end - end - end diff --git a/app/models/template.rb b/app/models/template.rb index 65788dc5b2..a339b6f55f 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -1,4 +1,3 @@ - # frozen_string_literal: true # == Schema Information @@ -33,10 +32,9 @@ # fk_rails_... (org_id => orgs.id) # +# Object that represents a DMP template # rubocop:disable Metrics/ClassLength - class Template < ApplicationRecord - include GlobalHelpers extend UniqueRandom @@ -48,14 +46,13 @@ class Template < ApplicationRecord # template should also always be publicly_visible. enum visibility: %i[organisationally_visible publicly_visible] - VISIBILITY_ORDER = %i[organisationally_visible publicly_visible] - # Stores links as an JSON object: # {funder: [{"link":"www.example.com","text":"foo"}, ...], # sample_plan: [{"link":"www.example.com","text":"foo"}, ...]} # # The links is validated against custom validator allocated at # validators/template_links_validator.rb + attribute :links, :text, default: { funder: [], sample_plan: [] } serialize :links, JSON attribute :published, :boolean, default: false @@ -64,10 +61,7 @@ class Template < ApplicationRecord attribute :version, :integer, default: 0 attribute :customization_of, :integer, default: nil attribute :family_id, :integer, default: -> { Template.new_family_id } - attribute :links, :text, default: { funder: [], sample_plan: [] } - # TODO: re-add visibility setting? (this is handled in org_admin/create and - # relies on the org_id in the current callback-form) - attribute :visibility, :integer, default: 0 + attribute :visibility, default: Template.visibilities[:organisationally_visible] # ================ # = Associations = @@ -140,7 +134,7 @@ class Template < ApplicationRecord .joins(<<~SQL) INNER JOIN templates ON current.version = templates.version AND current.family_id = templates.family_id - INNER JOIN orgs orgs_latest ON orgs_latest.id = templates.org_id + INNER JOIN orgs ON orgs.id = templates.org_id SQL } @@ -153,7 +147,7 @@ class Template < ApplicationRecord .joins(<<~SQL) INNER JOIN templates ON current.version = templates.version AND current.customization_of = templates.customization_of - INNER JOIN orgs org_customized ON org_customized.id = templates.org_id + INNER JOIN orgs ON orgs.id = templates.org_id SQL .where(templates: { org_id: org_id }) } @@ -174,7 +168,7 @@ class Template < ApplicationRecord family_ids = families(org_id).pluck(:family_id) latest_customized_version(family_ids, org_id) } - + # Retrieves templates with distinct family_id. It can be filtered down if # org_id is passed scope :families, lambda { |org_id = nil| @@ -189,21 +183,11 @@ class Template < ApplicationRecord # default template) scope :latest_customizable, lambda { funder_ids = Org.funder.pluck(:id) - # Make sure we include the default template - - # family_ids = families(funder_ids).distinct - # .pluck(:family_id) + [default.family_id] - - # published(family_ids.uniq) - # .where("visibility = :visibility", - # visibility: visibilities[:publicly_visible]) - - funder_ids = Org.funder.pluck(:id) - family_ids = families(funder_ids).distinct - .pluck(:family_id) + [default.family_id] - published(family_ids.uniq) - .where("visibility = :visibility OR is_default = :is_default", - visibility: visibilities[:publicly_visible], is_default: true) + family_ids = families(funder_ids).distinct + .pluck(:family_id) + [default.family_id] + published(family_ids.uniq) + .where('visibility = :visibility OR is_default = :is_default', + visibility: visibilities[:publicly_visible], is_default: true) } # Retrieves unarchived templates with public visibility @@ -218,19 +202,21 @@ class Template < ApplicationRecord unarchived.where(visibility: visibilities[:organisationally_visible]) } - # Retrieves unarchived templates whose title or org.name includes the term - # passed + # Retrieves unarchived templates whose title or org.name includes the term + # passed(We use search_term_orgs as alias for orgs to avoid issues with + # any orgs table join already present in loaded unarchived.) scope :search, lambda { |term| - unarchived.joins(<<~SQL) - LEFT JOIN orgs orgs_search ON templates.org_id = orgs_search.id - SQL - .where("lower(templates.title) LIKE lower(:term) OR " + - "lower(orgs_search.name) LIKE lower(:term)", - term: "%#{term}%") + unarchived + .joins(<<~SQL) + JOIN orgs AS search_term_orgs ON search_term_orgs.id = templates.org_id + SQL + .where('lower(templates.title) LIKE lower(:term)' \ + 'OR lower(search_term_orgs.name) LIKE lower(:term)', + term: "%#{term}%") } # defines the export setting for a template object - has_settings :export, class_name: "Settings::Template" do |s| + has_settings :export, class_name: 'Settings::Template' do |s| s.key :export, defaults: Settings::Template::DEFAULT_SETTINGS end @@ -262,7 +248,7 @@ def self.find_or_generate_version!(template) elsif template.latest? && !template.generate_version? template else - raise _("A historical template cannot be retrieved for being modified") + raise _('A historical template cannot be retrieved for being modified') end end @@ -272,14 +258,14 @@ def self.find_or_generate_version!(template) # Template::latest_version scope method for an adequate instantiation of # template instances. def self.latest_version_per_family(family_id = nil) - chained_scope = unarchived.select("MAX(version) AS version", :family_id) + chained_scope = unarchived.select('MAX(version) AS version', :family_id) chained_scope = chained_scope.where(family_id: family_id) if family_id.present? chained_scope.group(:family_id) end def self.latest_customized_version_per_customised_of(customization_of = nil, org_id = nil) - chained_scope = select("MAX(version) AS version", :customization_of) + chained_scope = select('MAX(version) AS version', :customization_of) chained_scope = chained_scope.where(customization_of: customization_of) chained_scope = chained_scope.where(org_id: org_id) if org_id.present? chained_scope.group(:customization_of) @@ -303,7 +289,8 @@ def description # Creates a copy of the current template # raises ActiveRecord::RecordInvalid when save option is true and validations # fails. - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def deep_copy(attributes: {}, **options) copy = dup if attributes.respond_to?(:each_pair) @@ -336,7 +323,8 @@ def deep_copy(attributes: {}, **options) copy end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # Retrieves the template's org or the org of the template this one is derived # from of it is a customization @@ -352,7 +340,7 @@ def base_org # # Returns Boolean def latest? - id == Template.latest_version(family_id).pluck("templates.id").first + id == Template.latest_version(family_id).pluck('templates.id').first end # Determines whether or not a new version should be generated @@ -363,7 +351,7 @@ def generate_version? # Determines whether or not a customization for the customizing_org passed # should be generated def customize?(customizing_org) - if customizing_org.is_a?(Org) && (org.funder? || is_default) + if customizing_org.is_a?(Org) && (org.funder_only? || is_default) return !Template.unarchived.where(customization_of: family_id, org: customizing_org).exists? end @@ -371,9 +359,9 @@ def customize?(customizing_org) end # Determines whether or not a customized template should be upgraded - def upgrade_customization? + def upgrade_customization? return false unless customization_of? - + funder_template = Template.published(customization_of).select(:created_at).first return false unless funder_template.present? @@ -395,49 +383,42 @@ def removable? # for the specified org def generate_copy!(org) # Assume customizing_org is persisted - raise _("generate_copy! requires an organisation target") unless org.is_a?(Org) + raise _('generate_copy! requires an organisation target') unless org.is_a?(Org) - template = deep_copy( + deep_copy( attributes: { version: 0, published: false, family_id: new_family_id, org: org, is_default: false, - title: _("Copy of %{template}") % { template: title } + title: format(_('Copy of %{template}'), template: title) }, modifiable: true, save: true ) - template end # Generates a new copy of self with an incremented version number def generate_version! - raise _("generate_version! requires a published template") unless published + raise _('generate_version! requires a published template') unless published - template = deep_copy( - attributes: { + deep_copy( + attributes: { version: version + 1, published: false, org: org - }, save: true + }, save: true ) - template end - - + # Generates a new copy of self for the specified customizing_org def customize!(customizing_org) # Assume customizing_org is persisted - unless customizing_org.is_a?(Org) - raise ArgumentError, _("customize! requires an organisation target") - end + raise ArgumentError, _('customize! requires an organisation target') unless customizing_org.is_a?(Org) # Assume self has org associated - unless org.funder? || is_default - raise ArgumentError, _("customize! requires a template from a funder") - end + raise ArgumentError, _('customize! requires a template from a funder') if !org.funder_only? && !is_default - customization = deep_copy( + deep_copy( attributes: { version: 0, published: false, @@ -448,7 +429,6 @@ def customize!(customizing_org) is_default: false }, modifiable: false, save: true ) - customization end # Generates a new copy of self including latest changes from the funder this @@ -466,54 +446,54 @@ def publish! end # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def publishability - error = "" + error = '' publishable = true # template must be the most recent draft if published - error += _("You can not publish a published template. ") + error += _('You can not publish a published template. ') publishable = false end - unless latest? - error += _("You can not publish a historical version of this template. ") - publishable = false + unless latest? + error += _('You can not publish a historical version of this template. ') + publishable = false # all templates have atleast one phase end - if phases.count <= 0 - error += _("You can not publish a template without phases. ") - publishable = false + if phases.count <= 0 + error += _('You can not publish a template without phases. ') + publishable = false # all phases must have atleast 1 section end - unless phases.map { |p| p.sections.count.positive? }.reduce(true) { |fin, val| fin and val } - error += _("You can not publish a template without sections in a phase. ") + unless phases.map { |p| p.sections.count.positive? }.reduce(true) { |fin, val| fin && val } + error += _('You can not publish a template without sections in a phase. ') publishable = false # all sections must have atleast one question end - unless sections.map { |s| s.questions.count.positive? }.reduce(true) { |fin, val| fin and val } - error += _("You can not publish a template without questions in a section. ") + unless sections.map { |s| s.questions.count.positive? }.reduce(true) { |fin, val| fin && val } + error += _('You can not publish a template without questions in a section. ') publishable = false end if invalid_condition_order - error += _("Conditions in the template refer backwards") + error += _('Conditions in the template refer backwards') publishable = false end [publishable, error] end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # TODO: refactor to use UniqueRandom # Generate a new random family identifier def self.new_family_id - family_id = loop do + loop do random = rand 2_147_483_647 break random unless Template.exists?(family_id: random) end - family_id end private - + # ============================ # = Private instance methods = # ============================ @@ -537,7 +517,7 @@ def invalid_condition_order next unless question.option_based? question.conditions.each do |condition| - next unless condition.action_type == "remove" + next unless condition.action_type == 'remove' condition.remove_data.each do |rem_id| rem_question = Question.find(rem_id.to_s) @@ -552,5 +532,5 @@ def before(question1, question2) question1.section.number < question2.section.number || (question1.section.number == question2.section.number && question1.number < question2.number) end - -end \ No newline at end of file +end +# rubocop:enable Metrics/ClassLength diff --git a/app/models/theme.rb b/app/models/theme.rb index c809fb949f..38fd801e76 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -12,14 +12,14 @@ # updated_at :datetime not null # +# Object that represents a question/guidance theme class Theme < ApplicationRecord - # ================ # = Associations = # ================ - has_and_belongs_to_many :questions, join_table: "questions_themes" - has_and_belongs_to_many :guidances, join_table: "themes_in_guidance" + has_and_belongs_to_many :questions, join_table: 'questions_themes' + has_and_belongs_to_many :guidances, join_table: 'themes_in_guidance' # =============== # = Validations = @@ -33,16 +33,17 @@ class Theme < ApplicationRecord scope :search, lambda { |term| search_pattern = "%#{term}%" - where("lower(title) LIKE lower(?) OR description LIKE lower(?)", + where('lower(title) LIKE lower(?) OR description LIKE lower(?)', search_pattern, search_pattern) } - scope :sorted_by_translated_title, -> { + # rubocop:disable Style/MultilineBlockChain, Style/BlockDelimiters + scope :sorted_by_translated_title, lambda { all.each { |theme| theme[:title] = _(theme[:title]) - }.sort_by { |theme| theme[:title] } + }.sort_by { |theme| theme[:title] } } - + # rubocop:enable Style/MultilineBlockChain, Style/BlockDelimiters # =========================== # = Public instance methods = @@ -65,5 +66,4 @@ def description def to_s title end - end diff --git a/app/models/token_permission_type.rb b/app/models/token_permission_type.rb index 57b558f81f..e030bebb04 100644 --- a/app/models/token_permission_type.rb +++ b/app/models/token_permission_type.rb @@ -11,33 +11,33 @@ # updated_at :datetime # +# Object that represents an API permission for V0 class TokenPermissionType < ApplicationRecord - # ============= # = Constants = # ============= ## # - GUIDANCES = TokenPermissionType.where(token_type: "guidances").first.freeze + GUIDANCES = TokenPermissionType.where(token_type: 'guidances').first.freeze ## # - PLANS = TokenPermissionType.where(token_type: "plans").first.freeze + PLANS = TokenPermissionType.where(token_type: 'plans').first.freeze ## # - TEMPLATES = TokenPermissionType.where(token_type: "templates").first.freeze + TEMPLATES = TokenPermissionType.where(token_type: 'templates').first.freeze ## # - STATISTICS = TokenPermissionType.where(token_type: "statistics").first.freeze + STATISTICS = TokenPermissionType.where(token_type: 'statistics').first.freeze # ================ # = Associations = # ================ - has_and_belongs_to_many :orgs, join_table: "org_token_permissions", unique: true + has_and_belongs_to_many :orgs, join_table: 'org_token_permissions', unique: true # ============== # = Validators = @@ -52,5 +52,4 @@ class TokenPermissionType < ApplicationRecord def to_s token_type end - end diff --git a/app/models/tracker.rb b/app/models/tracker.rb index 39628c5389..7e62838d12 100644 --- a/app/models/tracker.rb +++ b/app/models/tracker.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true +# Object that represents a Google Analytics tracker code class Tracker < ApplicationRecord - belongs_to :org validates :code, format: { with: /\A\z|\AUA-[0-9]+-[0-9]+\z/, - message: "wrong format" } - + message: 'wrong format' } end diff --git a/app/models/user.rb b/app/models/user.rb index 6bf7a75764..4c5c7f03ab 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -50,8 +50,9 @@ # fk_rails_... (language_id => languages.id) # fk_rails_... (org_id => orgs.id) # -class User < ApplicationRecord +# Object that represents a User +class User < ApplicationRecord include ConditionalUserMailer include DateRangeable include Identifiable @@ -99,7 +100,7 @@ class User < ApplicationRecord has_many :plans, through: :roles has_and_belongs_to_many :notifications, dependent: :destroy, - join_table: "notification_acknowledgements" + join_table: 'notification_acknowledgements' # =============== # = Validations = @@ -123,8 +124,8 @@ class User < ApplicationRecord # Retrieves all of the org_admins for the specified org scope :org_admins, lambda { |org_id| - joins(:perms).where("users.org_id = ? AND perms.name IN (?) AND " \ - "users.active = ?", + joins(:perms).where('users.org_id = ? AND perms.name IN (?) AND ' \ + 'users.active = ?', org_id, %w[grant_permissions modify_templates @@ -141,9 +142,9 @@ class User < ApplicationRecord # MySQL does not support standard string concatenation and since concat_ws # or concat functions do not exist for sqlite, we have to come up with this # conditional - if ActiveRecord::Base.connection.adapter_name == "Mysql2" + if mysql_db? where("lower(concat_ws(' ', firstname, surname)) LIKE lower(?) OR " \ - "lower(email) LIKE lower(?)", + 'lower(email) LIKE lower(?)', search_pattern, search_pattern) else joins(:org) @@ -176,7 +177,7 @@ class User < ApplicationRecord ## # Load the user based on the scheme and id provided by the Omniauth call def self.from_omniauth(auth) - Identifier.by_scheme_name(auth.provider.downcase, "User") + Identifier.by_scheme_name(auth.provider.downcase, 'User') .where(value: auth.uid) .first&.identifiable end @@ -216,6 +217,7 @@ def locale # user_email - Use the email if there is no firstname or surname (defaults: true) # # Returns String + # rubocop:disable Style/OptionalBooleanParameter def name(use_email = true) if (firstname.blank? && surname.blank?) || use_email email @@ -224,6 +226,7 @@ def name(use_email = true) name.strip end end + # rubocop:enable Style/OptionalBooleanParameter # The user's identifier for the specified scheme name # @@ -231,7 +234,7 @@ def name(use_email = true) # # Returns UserIdentifier def identifier_for(scheme) - identifiers.by_scheme_name(scheme, "User")&.first + identifiers.by_scheme_name(scheme, 'User')&.first end # Checks if the user is a super admin. If the user has any privelege which requires @@ -246,9 +249,10 @@ def can_super_admin? # requires them to see the org-admin pages then they are an org admin. # # Returns Boolean + # rubocop:disable Metrics/CyclomaticComplexity def can_org_admin? return true if can_super_admin? - + # Automatically false if the user has no Org or the Org is not managed return false unless org.present? && org.managed? @@ -256,7 +260,7 @@ def can_org_admin? can_modify_templates? || can_modify_org_details? || can_review_plans? end - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity # Can the User add new organisations? # @@ -345,7 +349,7 @@ def keep_or_generate_token! # Generates a new token def generate_token! - new_token = User.unique_random(field_name: "api_token") + new_token = User.unique_random(field_name: 'api_token') update_column(:api_token, new_token) end @@ -377,9 +381,8 @@ def get_preferences(key) # Override devise_invitable email title def deliver_invitation(options = {}) - super(options.merge(subject: _("A Data Management Plan in " \ - "%{application_name} has been shared with you") % - { application_name: ApplicationService.application_name }) + super(options.merge(subject: format(_('A Data Management Plan in %{application_name} has been shared with you'), + application_name: ApplicationService.application_name)) ) end @@ -392,9 +395,7 @@ def deliver_invitation(options = {}) # Returns ActiveRecord::Relation # Raises ArgumentError def self.where_case_insensitive(field, val) - unless columns.map(&:name).include?(field.to_s) - raise ArgumentError, "Field #{field} is not present on users table" - end + raise ArgumentError, "Field #{field} is not present on users table" unless columns.map(&:name).include?(field.to_s) User.where("LOWER(#{field}) = :value", value: val.to_s.downcase) end @@ -413,14 +414,13 @@ def acknowledge(notification) # leave account in-place, with org for statistics (until we refactor those) # # Returns boolean + # rubocop:disable Metrics/AbcSize def archive - # rubocop:disable Layout/LineLength - suffix = Rails.configuration.x.application.fetch(:archived_accounts_email_suffix, "@example.org") - # rubocop:enable Layout/LineLength - self.firstname = "Deleted" - self.surname = "User" - self.email = User.unique_random(field_name: "email", - prefix: "user_", + suffix = Rails.configuration.x.application.fetch(:archived_accounts_email_suffix, '@example.org') + self.firstname = 'Deleted' + self.surname = 'User' + self.email = User.unique_random(field_name: 'email', + prefix: 'user_', suffix: suffix, length: 5) self.recovery_email = nil @@ -431,7 +431,9 @@ def archive self.active = false save end + # rubocop:enable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize def merge(to_be_merged) scheme_ids = identifiers.pluck(:identifier_scheme_id) # merge logic @@ -449,6 +451,7 @@ def merge(to_be_merged) # => ignore any perms the deleted user has to_be_merged.destroy end + # rubocop:enable Metrics/AbcSize # For masking the ID that we send to rollbar # @@ -470,5 +473,4 @@ def delete_perms! def clear_department_id self.department_id = nil end - end diff --git a/app/models/user/at_csv.rb b/app/models/user/at_csv.rb index d0548bc44c..8cddb0f74e 100644 --- a/app/models/user/at_csv.rb +++ b/app/models/user/at_csv.rb @@ -1,16 +1,18 @@ # frozen_string_literal: true +# Helper for Admins class User - + # Helper to export a list of Users as CSV class AtCsv - - HEADERS = [_("Name"), _("E-Mail"), _("Created Date"), _("Last Activity"), _("Plans"), - _("Current Privileges"), _("Active"), _("Department")].freeze + HEADERS = [_('Name'), _('E-Mail'), _('Created Date'), _('Last Activity'), _('Plans'), + _('Current Privileges'), _('Active'), _('Department')].freeze def initialize(users) @users = users end + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def to_csv CSV.generate(headers: true) do |csv| csv << HEADERS @@ -20,24 +22,24 @@ def to_csv created = I18n.l user.created_at.to_date, format: :csv last_activity = I18n.l user.updated_at.to_date, format: :csv plans = user.plans.size - active = user.active ? "Yes" : "No" + active = user.active ? 'Yes' : 'No' current_privileges = if user.can_super_admin? - "Super Admin" + 'Super Admin' elsif user.can_org_admin? - "Organisational Admin" + 'Organisational Admin' else - "" + '' end - department = user&.department&.name || "" + department = user&.department&.name || '' csv << [name, email, created, last_activity, plans, current_privileges, active, department] end end end - + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity end - end diff --git a/app/policies/annotation_policy.rb b/app/policies/annotation_policy.rb index fbac714c9b..5b0c7d3f88 100644 --- a/app/policies/annotation_policy.rb +++ b/app/policies/annotation_policy.rb @@ -1,31 +1,22 @@ # frozen_string_literal: true +# Security rules for editing Annotations: Example Answers, Question level guidance +# Note the method names here correspond with controller actions class AnnotationPolicy < ApplicationPolicy - - attr_reader :user, :annotation - - def initialize(user, annotation) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @annotation = annotation - end + # NOTE: @user is the signed_in_user and @record is an instance of Annotation def create? - question = Question.find_by(id: @annotation.question_id) - if question.present? - return @user.can_modify_templates? && question.template.org_id == @user.org_id - end + question = Question.find_by(id: @record.question_id) + return @user.can_modify_templates? && question.template.org_id == @user.org_id if question.present? false end def update? - @user.can_modify_templates? && annotation.template.org_id == @user.org_id + @user.can_modify_templates? && @record.template.org_id == @user.org_id end def destroy? update? end - end diff --git a/app/policies/answer_policy.rb b/app/policies/answer_policy.rb index 34db43a008..11e687e197 100644 --- a/app/policies/answer_policy.rb +++ b/app/policies/answer_policy.rb @@ -1,21 +1,13 @@ # frozen_string_literal: true +# Security rules for answering questions +# Note the method names here correspond with controller actions class AnswerPolicy < ApplicationPolicy - - attr_reader :user - attr_reader :answer - - def initialize(user, answer) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @answer = answer - end + # NOTE: @user is the signed_in_user and @record is an instance of Answer def create_or_update? # TODO: Remove the owner check after the Roles have been updated # is the plan editable by the user or the user is the owner of the plan - @answer.plan.editable_by?(@user.id) || @user == @answer.plan.owner + @record.plan.editable_by?(@user.id) || @user == @record.plan.owner end - end diff --git a/app/policies/api/v0/departments_policy.rb b/app/policies/api/v0/departments_policy.rb index 18430ce13c..3c1c42f186 100644 --- a/app/policies/api/v0/departments_policy.rb +++ b/app/policies/api/v0/departments_policy.rb @@ -1,19 +1,10 @@ # frozen_string_literal: true module Api - module V0 - + # Security rules for API V0 Departments endpoints class DepartmentsPolicy < ApplicationPolicy - - attr_reader :user, :department - - def initialize(user, department) - raise Pundit::NotAuthorizedError, _("must be logged in") unless user - - @user = user - @department = department - end + # NOTE: @user is the signed_in_user and @record is an instance of Department ## # an org-admin can create a department for their organisation @@ -37,8 +28,8 @@ def users? # an org-admin may assign users (from their org) to a department (from their org) def assign_users? @user.can_org_admin? && - @department.present? && - @department.org == @user.org + @record.present? && + @record.org == @user.org end ## @@ -46,9 +37,6 @@ def assign_users? def unassign_users? @user.can_org_admin? end - end - end - end diff --git a/app/policies/api/v0/guidance_group_policy.rb b/app/policies/api/v0/guidance_group_policy.rb index d1db053c26..2d6af19a8f 100644 --- a/app/policies/api/v0/guidance_group_policy.rb +++ b/app/policies/api/v0/guidance_group_policy.rb @@ -1,27 +1,23 @@ # frozen_string_literal: true module Api - module V0 - + # Security rules for API V0 Guidance Group endpoints class GuidanceGroupPolicy < ApplicationPolicy - - attr_reader :user, :guidance_group + # NOTE: @user is the signed_in_user and @record is the guidance_group def initialize(user, guidance_group) - raise Pundit::NotAuthorizedError, _("must be logged in") unless user unless user.org.token_permission_types.include? TokenPermissionType::GUIDANCES - raise Pundit::NotAuthorizedError, _("must have access to guidances api") + raise Pundit::NotAuthorizedError, _('must have access to guidances api') end - @user = user - @guidance_group = guidance_group + super(user, guidance_group) end ## # is the plan editable by the user def show? - GuidanceGroup.can_view?(@user, @guidance_group) + GuidanceGroup.can_view?(@user, @record) end ## @@ -29,9 +25,6 @@ def show? def index? true end - end - end - end diff --git a/app/policies/api/v0/guidance_policy.rb b/app/policies/api/v0/guidance_policy.rb index 1562b5350e..a3853f6516 100644 --- a/app/policies/api/v0/guidance_policy.rb +++ b/app/policies/api/v0/guidance_policy.rb @@ -1,28 +1,23 @@ # frozen_string_literal: true module Api - module V0 - + # Security rules for API V0 Guidance endpoints class GuidancePolicy < ApplicationPolicy - - attr_reader :user - attr_reader :guidance + # NOTE: @user is the signed_in_user and @record is the guidance def initialize(user, guidance) - raise Pundit::NotAuthorizedError, _("must be logged in") unless user unless user.org.token_permission_types.include? TokenPermissionType::GUIDANCES - raise Pundit::NotAuthorizedError, _("must have access to guidances api") + raise Pundit::NotAuthorizedError, _('must have access to guidances api') end - @user = user - @guidance = guidance + super(user, guidance) end ## # is the plan editable by the user def show? - Guidance.can_view(@user, @guidance.id) + Guidance.can_view(@user, @record.id) end ## @@ -30,9 +25,6 @@ def show? def index? true end - end - end - end diff --git a/app/policies/api/v0/plans_policy.rb b/app/policies/api/v0/plans_policy.rb index 5d30b987ac..b1c313d7d7 100644 --- a/app/policies/api/v0/plans_policy.rb +++ b/app/policies/api/v0/plans_policy.rb @@ -1,36 +1,28 @@ # frozen_string_literal: true module Api - module V0 - + # Security rules for API V0 Plan endpoints class PlansPolicy < ApplicationPolicy + # NOTE: @user is the signed_in_user and @record is the plan - attr_reader :user - attr_reader :template - - def initialize(user, template) - raise Pundit::NotAuthorizedError, _("must be logged in") unless user + def initialize(user, plan) unless user.org.token_permission_types.include? TokenPermissionType::PLANS - raise Pundit::NotAuthorizedError, _("must have access to plans api") + raise Pundit::NotAuthorizedError, _('must have access to plans api') end - @user = user - @template = template + super(user, plan) end ## # users can create a plan if their template exists def create? - @template.present? + @record.present? end def index? @user.can_org_admin? end - end - end - end diff --git a/app/policies/api/v0/statistics_policy.rb b/app/policies/api/v0/statistics_policy.rb index 07a8db97d4..57996a7362 100644 --- a/app/policies/api/v0/statistics_policy.rb +++ b/app/policies/api/v0/statistics_policy.rb @@ -1,21 +1,17 @@ # frozen_string_literal: true module Api - module V0 - + # Security rules for API V0 Usage Statistic endpoints class StatisticsPolicy < ApplicationPolicy - - attr_reader :user + # NOTE: @user is the signed_in_user and @record is the statistic def initialize(user, statistic) - raise Pundit::NotAuthorizedError, _("must be logged in") unless user unless user.org.token_permission_types.include? TokenPermissionType::STATISTICS - raise Pundit::NotAuthorizedError, _("must have access to guidances api") + raise Pundit::NotAuthorizedError, _('must have access to guidances api') end - @user = user - @statistic = statistic + super(user, statistic) end ## @@ -31,7 +27,7 @@ def completed_plans? ## # need to check if your org owns this template def using_template? - @statistic.org_id == @user.org_id + @record.org_id == @user.org_id end ## @@ -45,9 +41,6 @@ def plans_by_template? def plans? true end - end - end - end diff --git a/app/policies/api/v0/template_policy.rb b/app/policies/api/v0/template_policy.rb index 48d33643ca..67f360ca13 100644 --- a/app/policies/api/v0/template_policy.rb +++ b/app/policies/api/v0/template_policy.rb @@ -1,21 +1,15 @@ # frozen_string_literal: true module Api - module V0 - + # Security rules for API V0 Template endpoints class TemplatePolicy < ApplicationPolicy - - attr_reader :user, :template - def initialize(user, template) - raise Pundit::NotAuthorizedError, _("must be logged in") unless user unless user.org.token_permission_types.include? TokenPermissionType::TEMPLATES - raise Pundit::NotAuthorizedError, _("must have access to guidances api") + raise Pundit::NotAuthorizedError, _('must have access to templates api') end - @user = user - @template = template + super(user, template) end ## @@ -23,9 +17,6 @@ def initialize(user, template) def index? true end - end - end - end diff --git a/app/policies/api/v1/plans_policy.rb b/app/policies/api/v1/plans_policy.rb index 32990de9b2..6286953840 100644 --- a/app/policies/api/v1/plans_policy.rb +++ b/app/policies/api/v1/plans_policy.rb @@ -1,52 +1,50 @@ # frozen_string_literal: true module Api - module V1 - + # Security rules for API V1 Plan endpoints class PlansPolicy < ApplicationPolicy + # NOTE: @user is either a User or an ApiClient - attr_reader :client, :plan - + # A helper method that takes the current client and returns the plans they + # have acess to class Scope - - attr_reader :client, :scope - - def initialize(client, scope) - @client = client - @scope = scope - end - ## return the visible plans (via the API) to a given client # ALL can view: public # ApiClient can view: anything from the API client # anything belonging to their Org (if applicable) # User (non-admin) can view: any personal or organisationally_visible # User (admin) can view: all from users of their organisation - # rubocop:disable Metrics/AbcSize def resolve - ids = Plan.publicly_visible.pluck(:id) - if client.is_a?(ApiClient) - ids += client.plans.pluck(&:id) - ids += client.org.plans.pluck(&:id) if client.org.present? - elsif client.is_a?(User) - ids += client.org.plans.organisationally_visible.pluck(:id) - ids += client.plans.pluck(:id) - ids += client.org.plans.pluck(:id) if client.can_org_admin? - end + ids = @user.is_a?(ApiClient) ? plans_for_client : plans_for_user Plan.where(id: ids.uniq) end - # rubocop:enable Metrics/AbcSize - end + private - def initialize(client, plan) - @client = client - @plan = plan - end + def plans_for_client + return [] unless @user.present? - end + ids = @user.plans.pluck(&:id) + ids += @user.org.plans.pluck(&:id) if @user.org.present? + ids + end - end + def plans_for_user + return [] unless @user.present? + + ids = @user.org.plans.organisationally_visible.pluck(:id) + ids += @user.plans.pluck(:id) + ids += @user.org.plans.pluck(:id) if @user.can_org_admin? + ids + end + def initialize(client, plan) + super() + @user = client + @plan = plan + end + end + end + end end diff --git a/app/policies/api_client_policy.rb b/app/policies/api_client_policy.rb index 8334fdee2a..e0da52d6a8 100644 --- a/app/policies/api_client_policy.rb +++ b/app/policies/api_client_policy.rb @@ -1,12 +1,9 @@ # frozen_string_literal: true +# Security rules for API Clients +# Note the method names here correspond with controller actions class ApiClientPolicy < ApplicationPolicy - - def initialize(user, *_args) - raise Pundit::NotAuthorizedError, _("must be logged in") unless user - - @user = user - end + # NOTE: @user is the signed_in_user def index? @user.can_super_admin? @@ -39,5 +36,4 @@ def refresh_credentials? def email_credentials? @user.can_super_admin? end - end diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index be9da399a9..1c514cb96e 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true +# Security rules for ?? Not sure where or how these are used, look like defaults class ApplicationPolicy - attr_reader :user, :record def initialize(user, record) - raise Pundit::NotAuthorizedError, "must be logged in" unless user + raise Pundit::NotAuthorizedError, 'must be logged in' unless user @user = user @record = record @@ -43,8 +43,8 @@ def scope Pundit.policy_scope!(user, record.class) end + # Default scope class Scope - attr_reader :user, :scope def initialize(user, scope) @@ -55,7 +55,5 @@ def initialize(user, scope) def resolve @scope = [] end - end - end diff --git a/app/policies/department_policy.rb b/app/policies/department_policy.rb index 3e865a9f95..95018eb5cf 100644 --- a/app/policies/department_policy.rb +++ b/app/policies/department_policy.rb @@ -1,15 +1,13 @@ # frozen_string_literal: true +# Security rules for department editing +# Note the method names here correspond with controller actions class DepartmentPolicy < ApplicationPolicy + # NOTE: @user is the signed_in_user and @record is an instance of Department - attr_reader :user - attr_reader :department - - def initialize(user, department) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @department = department + def index? + (@user.can_org_admin? && @user.org.id == @department.org_id) || + @user.can_super_admin? end def new? @@ -21,18 +19,17 @@ def create? end def edit? - (@user.can_org_admin? && @user.org.id == @department.org_id) || + (@user.can_org_admin? && @user.org.id == @record.org_id) || @user.can_super_admin? end def update? - (@user.can_org_admin? && @user.org.id == @department.org_id) || + (@user.can_org_admin? && @user.org.id == @record.org_id) || @user.can_super_admin? end def destroy? - (@user.can_org_admin? && @user.org.id == @department.org_id) || + (@user.can_org_admin? && @user.org.id == @record.org_id) || @user.can_super_admin? end - end diff --git a/app/policies/guidance_group_policy.rb b/app/policies/guidance_group_policy.rb index d282310303..3b6fc21376 100644 --- a/app/policies/guidance_group_policy.rb +++ b/app/policies/guidance_group_policy.rb @@ -1,54 +1,46 @@ # frozen_string_literal: true +# Security rules for guidance group editing +# Note the method names here correspond with controller actions class GuidanceGroupPolicy < ApplicationPolicy - - attr_reader :user, :guidance_group - - def initialize(user, guidance_group) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @guidance_group = guidance_group - end + # NOTE: @user is the signed_in_user and @record is an instance of GuidanceGroup def admin_show? - user.can_modify_guidance? && (guidance_group.org_id == user.org_id) + @user.can_modify_guidance? && (@record.org_id == @user.org_id) end def admin_edit? - user.can_modify_guidance? && (guidance_group.org_id == user.org_id) + @user.can_modify_guidance? && (@record.org_id == @user.org_id) end def admin_update? - user.can_modify_guidance? && (guidance_group.org_id == user.org_id) + @user.can_modify_guidance? && (@record.org_id == @user.org_id) end def admin_update_publish? - user.can_modify_guidance? && (guidance_group.org_id == user.org_id) + @user.can_modify_guidance? && (@record.org_id == @user.org_id) end def admin_update_unpublish? - user.can_modify_guidance? && (guidance_group.org_id == user.org_id) + @user.can_modify_guidance? && (@record.org_id == @user.org_id) end def admin_new? - user.can_modify_guidance? + @user.can_modify_guidance? end def admin_create? - user.can_modify_guidance? + @user.can_modify_guidance? end def admin_destroy? - user.can_modify_guidance? && (guidance_group.org_id == user.org_id) + @user.can_modify_guidance? && (@record.org_id == @user.org_id) end + # Returns the guidance groups for the specified org class Scope < Scope - def resolve - scope.where(org_id: user.org_id) + scope.where(org_id: @user.org_id) end - end - end diff --git a/app/policies/guidance_policy.rb b/app/policies/guidance_policy.rb index d95d3cdad4..7856dfc39d 100644 --- a/app/policies/guidance_policy.rb +++ b/app/policies/guidance_policy.rb @@ -1,26 +1,20 @@ # frozen_string_literal: true +# Security rules for guidance +# Note the method names here correspond with controller actions class GuidancePolicy < ApplicationPolicy - - attr_reader :user, :guidance - - def initialize(user, guidance) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @guidance = guidance - end + # NOTE: @user is the signed_in_user and @record is an instance of Guidance def admin_show? - user.can_modify_guidance? && guidance.in_group_belonging_to?(user.org_id) + @user.can_modify_guidance? && @record.in_group_belonging_to?(@user.org_id) end def admin_edit? - user.can_modify_guidance? && guidance.in_group_belonging_to?(user.org_id) + @user.can_modify_guidance? && @record.in_group_belonging_to?(@user.org_id) end def admin_update? - user.can_modify_guidance? && guidance.in_group_belonging_to?(user.org_id) + @user.can_modify_guidance? && @record.in_group_belonging_to?(@user.org_id) end def index? @@ -28,27 +22,26 @@ def index? end def admin_index? - user.can_modify_guidance? + @user.can_modify_guidance? end def admin_new? - user.can_modify_guidance? + @user.can_modify_guidance? end def admin_create? - user.can_modify_guidance? + @user.can_modify_guidance? end def admin_destroy? - user.can_modify_guidance? && guidance.in_group_belonging_to?(user.org_id) + @user.can_modify_guidance? && @record.in_group_belonging_to?(@user.org_id) end def admin_publish? - user.can_modify_guidance? + @user.can_modify_guidance? end def admin_unpublish? - user.can_modify_guidance? + @user.can_modify_guidance? end - end diff --git a/app/policies/identifier_policy.rb b/app/policies/identifier_policy.rb index d9afca3286..ce5929dcc9 100644 --- a/app/policies/identifier_policy.rb +++ b/app/policies/identifier_policy.rb @@ -1,24 +1,18 @@ # frozen_string_literal: true +# Security rules for un-associating a user from their Shib or ORCID +# Note the method names here correspond with controller actions class IdentifierPolicy < ApplicationPolicy - - def initialize(user, users) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @users = users - end + # NOTE: @user is the signed_in_user def destroy? - !user.nil? + !@user.nil? end + # Returns the identifiers for the user class Scope < Scope - def resolve - scope.where(user_id: user.id) + @scope.where(user_id: @user.id) end - end - end diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index 98d4eb5a01..0b3b4dc562 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -1,27 +1,19 @@ # frozen_string_literal: true +# Security rules for comments +# Note the method names here correspond with controller actions class NotePolicy < ApplicationPolicy - - attr_reader :user - attr_reader :note - - def initialize(user, note) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @note = note - end + # NOTE: @user is the signed_in_user and @record is an instance of Note def create? - @note.answer.plan.commentable_by?(@user.id) + @record.answer.plan.commentable_by?(@user.id) end def update? - Plan.find(@note.answer.plan_id).commentable_by?(@user.id) && @note.user_id == @user.id + Plan.find(@record.answer.plan_id).commentable_by?(@user.id) && @record.user_id == @user.id end def archive? - Plan.find(@note.answer.plan_id).commentable_by?(@user.id) + Plan.find(@record.answer.plan_id).commentable_by?(@user.id) end - end diff --git a/app/policies/notification_policy.rb b/app/policies/notification_policy.rb index 9f24481b8c..0397c21286 100644 --- a/app/policies/notification_policy.rb +++ b/app/policies/notification_policy.rb @@ -1,12 +1,9 @@ # frozen_string_literal: true +# Security rules for system wide notifications +# Note the method names here correspond with controller actions class NotificationPolicy < ApplicationPolicy - - def initialize(user, *_args) - raise Pundit::NotAuthorizedError, _("must be logged in") unless user - - @user = user - end + # NOTE: @user is the signed_in_user def index? @user.can_super_admin? @@ -35,5 +32,4 @@ def destroy? def enable? @user.can_super_admin? end - end diff --git a/app/policies/org_policy.rb b/app/policies/org_policy.rb index 8a927ced14..c27c78c8e0 100644 --- a/app/policies/org_policy.rb +++ b/app/policies/org_policy.rb @@ -1,42 +1,36 @@ # frozen_string_literal: true +# Security rules for orgs +# Note the method names here correspond with controller actions class OrgPolicy < ApplicationPolicy - - attr_reader :user, :org - - def initialize(user, org) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @org = org - end + # NOTE: @user is the signed_in_user and @record is an instance of Org def admin_show? - user.can_modify_org_details? && (user.org_id == org.id) + @user.can_modify_org_details? && (@user.org_id == @record.id) end def admin_edit? - user.can_modify_org_details? && (user.org_id == org.id || user.can_super_admin?) + @user.can_modify_org_details? && (@user.org_id == @record.id || @user.can_super_admin?) end def admin_update? - user.can_modify_org_details? && (user.org_id == org.id || user.can_super_admin?) + @user.can_modify_org_details? && (@user.org_id == @record.id || @user.can_super_admin?) end def index? - user.can_super_admin? + @user.can_super_admin? end def new? - user.can_super_admin? + @user.can_super_admin? end def create? - user.can_super_admin? + @user.can_super_admin? end def destroy? - user.can_super_admin? + @user.can_super_admin? end def parent? @@ -52,11 +46,10 @@ def templates? end def merge_analyze? - user.can_super_admin? + @user.can_super_admin? end def merge_commit? - user.can_super_admin? + @user.can_super_admin? end - end diff --git a/app/policies/paginable/plan_policy.rb b/app/policies/paginable/plan_policy.rb index 0008443321..69d9683af0 100644 --- a/app/policies/paginable/plan_policy.rb +++ b/app/policies/paginable/plan_policy.rb @@ -1,12 +1,9 @@ # frozen_string_literal: true module Paginable - + # Security rules for plan tables class PlanPolicy < ApplicationPolicy - - def initialize(user) - @user = user - end + # NOTE: @user is the signed_in_user def privately_visible? @user.is_a?(User) @@ -15,7 +12,5 @@ def privately_visible? def organisationally_or_publicly_visible? @user.is_a?(User) end - end - end diff --git a/app/policies/phase_policy.rb b/app/policies/phase_policy.rb index 5ccb6bf384..5d2b683162 100644 --- a/app/policies/phase_policy.rb +++ b/app/policies/phase_policy.rb @@ -1,15 +1,9 @@ # frozen_string_literal: true +# Security rules for template phases +# Note the method names here correspond with controller actions class PhasePolicy < ApplicationPolicy - - attr_reader :user, :phase - - def initialize(user, phase) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @phase = phase - end + # NOTE: @user is the signed_in_user and @record is an instance of Phase ## # Org-admin side @@ -18,31 +12,34 @@ def initialize(user, phase) # - The template which they are modifying belongs to their org def show? - user.can_modify_templates? && (phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.template.org_id == @user.org_id) end def preview? - user.can_modify_templates? && (phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.template.org_id == @user.org_id) + end + + def edit? + user.can_modify_templates? && (@record.template.org_id == user.org_id) end def update? - user.can_modify_templates? && (phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.template.org_id == @user.org_id) end def new? - user.can_modify_templates? && (phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.template.org_id == @user.org_id) end def create? - user.can_modify_templates? && (phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.template.org_id == @user.org_id) end def destroy? - user.can_modify_templates? && (phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.template.org_id == @user.org_id) end def sort? - user.can_modify_templates? && (phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.template.org_id == @user.org_id) end - end diff --git a/app/policies/plan_policy.rb b/app/policies/plan_policy.rb index 3ea2637022..7dd5530d04 100644 --- a/app/policies/plan_policy.rb +++ b/app/policies/plan_policy.rb @@ -1,85 +1,85 @@ # frozen_string_literal: true +# Security rules for plans +# Note the method names here correspond with controller actions class PlanPolicy < ApplicationPolicy + # NOTE: @user is the signed_in_user and @record is an instance of Plan - attr_reader :user - attr_reader :plan - - def initialize(user, plan) - raise Pundit::NotAuthorizedError, _("must be logged in") unless user - - unless plan || plan.publicly_visible? - raise Pundit::NotAuthorizedError, - _("are not authorized to view that plan") - end - @user = user - @plan = plan + def index? + @user.present? end def show? - @plan.readable_by?(@user.id) + @record.readable_by?(@user.id) end def share? - @plan.editable_by?(@user.id) || + @record.editable_by?(@user.id) || (@user.can_org_admin? && - @user.org.plans.include?(@plan)) + @user.org.plans.include?(@record)) end def export? - @plan.readable_by?(@user.id) + @record.readable_by?(@user.id) end def download? - @plan.readable_by?(@user.id) + @record.readable_by?(@user.id) end def edit? - @plan.readable_by?(@user.id) + @record.readable_by?(@user.id) end def update? - @plan.editable_by?(@user.id) + @record.editable_by?(@user.id) end def destroy? - @plan.editable_by?(@user.id) + @record.editable_by?(@user.id) end def status? - @plan.readable_by?(@user.id) + @record.readable_by?(@user.id) end def duplicate? - @plan.editable_by?(@user.id) + @record.editable_by?(@user.id) end def visibility? - @plan.administerable_by?(@user.id) + @record.administerable_by?(@user.id) end def set_test? - @plan.administerable_by?(@user.id) + @record.administerable_by?(@user.id) end def answer? - @plan.readable_by?(@user.id) + @record.readable_by?(@user.id) end def request_feedback? - @plan.administerable_by?(@user.id) + @record.administerable_by?(@user.id) end def overview? - @plan.readable_by?(@user.id) + @record.readable_by?(@user.id) end def select_guidances_list? - @plan.readable_by?(@user.id) + @record.readable_by?(@user.id) end def update_guidances_list? - @plan.editable_by?(@user.id) + @record.editable_by?(@user.id) end + def privately_visible? + @user.present? + end + + def organisationally_or_publicly_visible? + @user.present? + end end diff --git a/app/policies/public_page_policy.rb b/app/policies/public_page_policy.rb index b75cb66ece..76160bac20 100644 --- a/app/policies/public_page_policy.rb +++ b/app/policies/public_page_policy.rb @@ -1,11 +1,14 @@ # frozen_string_literal: true +# Security rules for the public pages +# Note the method names here correspond with controller actions class PublicPagePolicy < ApplicationPolicy - - def initialize(object, object2 = nil) - @object = object - @object2 = object2 + # rubocop:disable Lint/MissingSuper + def initialize(user, record = nil) + @user = user + @record = record end + # rubocop:enable Lint/MissingSuper def plan_index? true @@ -16,22 +19,20 @@ def template_index? end def template_export? - @object.present? && @object.published? + @user.present? && @record.published? end def plan_export? - @object.publicly_visible? + @record.publicly_visible? end def plan_organisationally_exportable? - plan = @object - user = @object2 - if plan.is_a?(Plan) && user.is_a?(User) - return plan.publicly_visible? || (plan.organisationally_visible? && plan.owner.present? && - plan.owner.org_id == user.org_id) + if @record.is_a?(Plan) && @user.is_a?(User) + return @record.publicly_visible? || + (@record.organisationally_visible? && @record.owner.present? && + @record.owner.org_id == @user.org_id) end false end - end diff --git a/app/policies/question_option_policy.rb b/app/policies/question_option_policy.rb index a7fc9d3bc4..573dc0d054 100644 --- a/app/policies/question_option_policy.rb +++ b/app/policies/question_option_policy.rb @@ -1,15 +1,9 @@ # frozen_string_literal: true +# Security rules for options of multi select questions +# Note the method names here correspond with controller actions class QuestionOptionPolicy < ApplicationPolicy - - attr_reader :user, :question_option - - def initialize(user, question_option) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @question_option = question_option - end + # NOTE: @user is the signed_in_user and @record is an instance of QuestionOption ## # The only action specifically on question_options is delete. @@ -18,10 +12,8 @@ def initialize(user, question_option) # - They can modify templates # - The template which they are modifying belongs to their org ## - def destroy? - user.can_modify_templates? && - (question_option.question.section.phase.template.org_id == user.org_id) + @user.can_modify_templates? && + (@record.question.section.phase.template.org_id == @user.org_id) end - end diff --git a/app/policies/question_policy.rb b/app/policies/question_policy.rb index f290368045..f3440b5536 100644 --- a/app/policies/question_policy.rb +++ b/app/policies/question_policy.rb @@ -1,57 +1,49 @@ # frozen_string_literal: true +# Security rules for questions +# Note the method names here correspond with controller actions class QuestionPolicy < ApplicationPolicy - - attr_reader :user, :question - - def initialize(user, question) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @question = question - end + # NOTE: @user is the signed_in_user and @record is an instance of Question ## # Users can modify questions if: # - They can modify templates # - The template which they are modifying belongs to their org ## - def index? - user.present? + @user.present? end def show? - user.present? + @user.present? end def open_conditions? - user.can_modify_templates? && (question.section.phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.section.phase.template.org_id == @user.org_id) end def edit? - user.can_modify_templates? && (question.section.phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.section.phase.template.org_id == @user.org_id) end def new? - user.can_modify_templates? && (question.section.phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.section.phase.template.org_id == @user.org_id) end def create? - user.can_modify_templates? && (question.section.phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.section.phase.template.org_id == @user.org_id) end def update? - user.can_modify_templates? && (question.section.phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.section.phase.template.org_id == @user.org_id) end def destroy? - user.can_modify_templates? && (question.section.phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.section.phase.template.org_id == @user.org_id) end # TODO: Remove this after annotations controller is refactored def admin_update? - user.can_modify_templates? && (question.section.phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.section.phase.template.org_id == @user.org_id) end - end diff --git a/app/policies/research_output_policy.rb b/app/policies/research_output_policy.rb new file mode 100644 index 0000000000..defe86a0c9 --- /dev/null +++ b/app/policies/research_output_policy.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# Security policies for research outputs +class ResearchOutputPolicy < ApplicationPolicy + attr_reader :user, :research_output + + def initialize(user, research_output) + raise Pundit::NotAuthorizedError, _('must be logged in') unless user + + raise Pundit::NotAuthorizedError, _('are not authorized to view that plan') unless research_output.present? + + @user = user + @research_output = research_output + super + end + + def index? + @research_output.plan.readable_by?(@user.id) + end + + def new? + @research_output.plan.administerable_by?(@user.id) + end + + def edit? + @research_output.plan.administerable_by?(@user.id) + end + + def create? + @research_output.plan.administerable_by?(@user.id) + end + + def update? + @research_output.plan.administerable_by?(@user.id) + end + + def destroy? + @research_output.plan.administerable_by?(@user.id) + end + + def select_output_type? + @research_output.plan.administerable_by?(@user.id) + end + + def select_license? + @research_output.plan.administerable_by?(@user.id) + end + + def repository_search? + @research_output.plan.administerable_by?(@user.id) + end + + def metadata_standard_search? + @research_output.plan.administerable_by?(@user.id) + end +end diff --git a/app/policies/role_policy.rb b/app/policies/role_policy.rb index a44f69a60d..17ffe5a995 100644 --- a/app/policies/role_policy.rb +++ b/app/policies/role_policy.rb @@ -1,31 +1,23 @@ # frozen_string_literal: true +# Security rules for changing a Users role on a plan from the collaborators section +# Note the method names here correspond with controller actions class RolePolicy < ApplicationPolicy - - attr_reader :user - attr_reader :role - - def initialize(user, role) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @role = role - end + # NOTE: @user is the signed_in_user and @record is an instance of Role def create? - @role.plan.administerable_by?(@user.id) + @record.plan.administerable_by?(@user.id) end def update? - @role.plan.administerable_by?(@user.id) + @record.plan.administerable_by?(@user.id) end def destroy? - @role.plan.administerable_by?(@user.id) + @record.plan.administerable_by?(@user.id) end def deactivate? - @role.user_id == @user.id + @record.user_id == @user.id end - end diff --git a/app/policies/section_policy.rb b/app/policies/section_policy.rb index 58c14a62ac..501745f48a 100644 --- a/app/policies/section_policy.rb +++ b/app/policies/section_policy.rb @@ -1,48 +1,40 @@ # frozen_string_literal: true +# Security rules for template sections +# Note the method names here correspond with controller actions class SectionPolicy < ApplicationPolicy - - attr_reader :user, :section - - def initialize(user, section) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @section = section - end + # NOTE: @user is the signed_in_user and @record is an instance of Section ## # Users can modify sections if: # - They can modify templates # - The template which they are modifying belongs to their org ## - def index? - user.present? + @user.present? end def show? - user.present? + @user.present? end def edit? - user.can_modify_templates? && (section.phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.phase.template.org_id == @user.org_id) end def new? - user.can_modify_templates? && (section.phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.phase.template.org_id == @user.org_id) end def create? - user.can_modify_templates? && (section.phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.phase.template.org_id == @user.org_id) end def update? - user.can_modify_templates? && (section.phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.phase.template.org_id == @user.org_id) end def destroy? - user.can_modify_templates? && (section.phase.template.org_id == user.org_id) + @user.can_modify_templates? && (@record.phase.template.org_id == @user.org_id) end - end diff --git a/app/policies/settings/plan_policy.rb b/app/policies/settings/plan_policy.rb index b343f1489c..e85b0168d2 100644 --- a/app/policies/settings/plan_policy.rb +++ b/app/policies/settings/plan_policy.rb @@ -1,23 +1,16 @@ # frozen_string_literal: true -class Settings::PlanPolicy < ApplicationPolicy +module Settings + # Security rules plan export settings + class PlanPolicy < ApplicationPolicy + # NOTE: @user is the signed_in_user and @record is an instance of Plan - attr_reader :user - attr_reader :plan + def show? + @record.readable_by(@user.id) + end - def initialize(user, plan) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @plan = plan - end - - def show? - @plan.readable_by(@user.id) + def update? + @record.editable_by(@user.id) + end end - - def update? - @plan.editable_by(@user.id) - end - end diff --git a/app/policies/settings/project_policy.rb b/app/policies/settings/project_policy.rb index bd091d2a2c..5a5fbe8c0c 100644 --- a/app/policies/settings/project_policy.rb +++ b/app/policies/settings/project_policy.rb @@ -1,28 +1,17 @@ # frozen_string_literal: true -class Settings::ProjectPolicy < ApplicationPolicy - - # this is the policy for app/controllers/settings/projects_controller.rb - - attr_reader :user - attr_reader :projects - - def initialize(user, settings) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @settings = settings - end - - # for this controller, we allow all actions as the "settings" object - # is curated by rails based on user, not on a passed param - - def show? - true +module Settings + # Security rules project export settings + class ProjectPolicy < ApplicationPolicy + # this is the policy for app/controllers/settings/projects_controller.rb + # for this controller, we allow all actions as the "settings" object + # is curated by rails based on user, not on a passed param + def show? + true + end + + def update? + true + end end - - def update? - true - end - end diff --git a/app/policies/template_policy.rb b/app/policies/template_policy.rb index 7a30916a23..edceac110f 100644 --- a/app/policies/template_policy.rb +++ b/app/policies/template_policy.rb @@ -1,79 +1,73 @@ # frozen_string_literal: true +# Security rules for templates +# Note the method names here correspond with controller actions class TemplatePolicy < ApplicationPolicy - - attr_reader :user, :template - - def initialize(user, template = Template.new) - raise Pundit::NotAuthorizedError, _("must be logged in") unless user.is_a?(User) - - @user = user - @template = template - end + # NOTE: @user is the signed_in_user and @record is an instance of Template def index? - user.can_super_admin? + @user.can_super_admin? end def organisational? - user.can_modify_templates? + @user.can_modify_templates? end def customisable? - user.can_modify_templates? + @user.can_modify_templates? end def new? - user.can_super_admin? || user.can_modify_templates? + @user.can_super_admin? || @user.can_modify_templates? end def create? - user.can_super_admin? || user.can_modify_templates? + @user.can_super_admin? || @user.can_modify_templates? end def show? - user.can_super_admin? || (user.can_modify_templates? && template.org_id == user.org_id) + @user.can_super_admin? || (@user.can_modify_templates? && @record.org_id == @user.org_id) end def edit? - user.can_super_admin? || (user.can_modify_templates? && template.org_id == user.org_id) + @user.can_super_admin? || (@user.can_modify_templates? && @record.org_id == @user.org_id) end def update? - user.can_super_admin? || (user.can_modify_templates? && template.org_id == user.org_id) + @user.can_super_admin? || (@user.can_modify_templates? && @record.org_id == @user.org_id) end def destroy? - user.can_super_admin? || (user.can_modify_templates? && (template.org_id == user.org_id)) + @user.can_super_admin? || (@user.can_modify_templates? && (@record.org_id == @user.org_id)) end def history? - user.can_super_admin? || (user.can_modify_templates? && template.org_id == user.org_id) + @user.can_super_admin? || (@user.can_modify_templates? && @record.org_id == @user.org_id) end def customize? - user.can_super_admin? || user.can_modify_templates? + @user.can_super_admin? || @user.can_modify_templates? end def transfer_customization? - user.can_super_admin? || user.can_modify_templates? + @user.can_super_admin? || @user.can_modify_templates? end def template_export? - user.can_super_admin? || (user.can_modify_templates? && (template.org_id == user.org_id)) + @user.can_super_admin? || (@user.can_modify_templates? && (@record.org_id == @user.org_id)) end # AJAX Calls def copy? - user.can_super_admin? || (user.can_modify_templates? && (template.org_id == user.org_id)) + @user.can_super_admin? || (@user.can_modify_templates? && (@record.org_id == @user.org_id)) end def publish? - user.can_super_admin? || (user.can_modify_templates? && (template.org_id == user.org_id)) + @user.can_super_admin? || (@user.can_modify_templates? && (@record.org_id == @user.org_id)) end def unpublish? - user.can_super_admin? || (user.can_modify_templates? && (template.org_id == user.org_id)) + @user.can_super_admin? || (@user.can_modify_templates? && (@record.org_id == @user.org_id)) end ## @@ -85,7 +79,6 @@ def unpublish? # Anyone with an account should be able to get templates for the sepecified research_org + funder # This policy is applicable to the Create Plan page def template_options? - user.present? + @user.present? end - end diff --git a/app/policies/theme_policy.rb b/app/policies/theme_policy.rb index 4c44add700..9643db4ea5 100644 --- a/app/policies/theme_policy.rb +++ b/app/policies/theme_policy.rb @@ -1,12 +1,9 @@ # frozen_string_literal: true +# Security rules for editing themes +# Note the method names here correspond with controller actions class ThemePolicy < ApplicationPolicy - - def initialize(user, *_args) - raise Pundit::NotAuthorizedError, _("must be logged in") unless user - - @user = user - end + # NOTE: @user is the signed_in_user def index? @user.can_super_admin? @@ -31,5 +28,4 @@ def update? def destroy? @user.can_super_admin? end - end diff --git a/app/policies/token_permission_type_policy.rb b/app/policies/token_permission_type_policy.rb index 99c2d77c64..eb5b5f4372 100644 --- a/app/policies/token_permission_type_policy.rb +++ b/app/policies/token_permission_type_policy.rb @@ -1,18 +1,11 @@ # frozen_string_literal: true +# Security rules for viewing API V0 token permission types +# Note the method names here correspond with controller actions class TokenPermissionTypePolicy < ApplicationPolicy - - attr_reader :user, :token_permission_type - - def initialize(user, token_permission_type) - raise Pundit::NotAuthorizedError, "must be logged in" unless user - - @user = user - @token_permission_type = token_permission_type - end + # NOTE: @user is the signed_in_user def index? - user.can_use_api? && user.org.token_permission_types.count.positive? + @user.can_use_api? && @user.org.token_permission_types.count.positive? end - end diff --git a/app/policies/usage_policy.rb b/app/policies/usage_policy.rb index 926f25cb70..298dcb9619 100644 --- a/app/policies/usage_policy.rb +++ b/app/policies/usage_policy.rb @@ -4,12 +4,12 @@ # a policy that is not associated with a model (per the pundit README) # rubocop:disable Style/StructInheritance class UsagePolicy < Struct.new(:user, :usage) - attr_reader :user - def initialize(user, _usage) - raise Pundit::NotAuthorizedError, "must be logged in" unless user + def initialize(user, usage) + raise Pundit::NotAuthorizedError, 'must be logged in' unless user + super(user, usage) @user = user end @@ -44,6 +44,5 @@ def yearly_plans? def filter? @user.can_org_admin? end - end # rubocop:enable Style/StructInheritance diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 0a404c34a5..5a1e2c4dba 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -1,54 +1,45 @@ # frozen_string_literal: true +# Security rules for users +# Note the method names here correspond with controller actions class UserPolicy < ApplicationPolicy - - attr_reader :signed_in_user - attr_reader :user - - def initialize(signed_in_user, user) - raise Pundit::NotAuthorizedError, "must be logged in" unless signed_in_user - - @signed_in_user = signed_in_user - @user = user - end - def index? admin_index? end def admin_index? - signed_in_user.can_grant_permissions? + @user.can_grant_permissions? end def admin_grant_permissions? - (signed_in_user.can_grant_permissions? && user.org_id == signed_in_user.org_id) || - signed_in_user.can_super_admin? + (@user.can_grant_permissions? && user.org_id == @user.org_id) || + @user.can_super_admin? end def admin_update_permissions? - (signed_in_user.can_grant_permissions? && user.org_id == signed_in_user.org_id) || - signed_in_user.can_super_admin? + (@user.can_grant_permissions? && user.org_id == @user.org_id) || + @user.can_super_admin? end # Allows the user to swap their org affiliation on the fly def org_swap? - signed_in_user.can_super_admin? + @user.can_super_admin? end def activate? - signed_in_user.can_super_admin? + @user.can_super_admin? end def edit? - signed_in_user.can_super_admin? || signed_in_user.can_org_admin? + @user.can_super_admin? || @user.can_org_admin? end def update? - signed_in_user.can_super_admin? || signed_in_user.can_org_admin? + @user.can_super_admin? || @user.can_org_admin? end def user_plans? - signed_in_user.can_super_admin? || signed_in_user.can_org_admin? + @user.can_super_admin? || @user.can_org_admin? end def update_email_preferences? @@ -60,32 +51,30 @@ def acknowledge_notification? end def refresh_token? - signed_in_user.can_super_admin? || - (signed_in_user.can_org_admin? && signed_in_user.can_use_api?) + @user.can_super_admin? || + (@user.can_org_admin? && @user.can_use_api?) end def merge? - signed_in_user.can_super_admin? + @user.can_super_admin? end def archive? - signed_in_user.can_super_admin? + @user.can_super_admin? end def search? - signed_in_user.can_super_admin? + @user.can_super_admin? end def org_admin_other_user? - signed_in_user.can_super_admin? || signed_in_user.can_org_admin? + @user.can_super_admin? || @user.can_org_admin? end + # returns the users for the org class Scope < Scope - def resolve - scope.where(org_id: user.org_id) + @scope.where(org_id: @user.org_id) end - end - end diff --git a/app/presenters/api/v1/api_presenter.rb b/app/presenters/api/v1/api_presenter.rb new file mode 100644 index 0000000000..692adef0cf --- /dev/null +++ b/app/presenters/api/v1/api_presenter.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Api + module V1 + # Generic helper methods for API V1 + 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/v1/contributor_presenter.rb b/app/presenters/api/v1/contributor_presenter.rb index 6970433bd7..4ed0c1ca68 100644 --- a/app/presenters/api/v1/contributor_presenter.rb +++ b/app/presenters/api/v1/contributor_presenter.rb @@ -1,29 +1,22 @@ # frozen_string_literal: true module Api - module V1 - + # Helper class for the API V1 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.downcase == "other" + return 'other' if role.to_s.downcase == 'other' "#{Contributor::ONTOLOGY_BASE_URL}/#{role.to_s.downcase.gsub('_', '-')}" end def contributor_id(identifiers:) - identifiers.select { |id| id.identifier_scheme.name == "orcid" }.first + identifiers.select { |id| id.identifier_scheme.name == 'orcid' }.first end - end - end - end - end diff --git a/app/presenters/api/v1/funding_presenter.rb b/app/presenters/api/v1/funding_presenter.rb index 2216ace28c..d2c221630a 100644 --- a/app/presenters/api/v1/funding_presenter.rb +++ b/app/presenters/api/v1/funding_presenter.rb @@ -1,32 +1,25 @@ # frozen_string_literal: true module Api - module V1 - + # Helper class for the API V1 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? + return 'planned' unless plan.present? case plan.funding_status - when "funded" - "granted" - when "denied" - "rejected" + when 'funded' + 'granted' + when 'denied' + 'rejected' else - "planned" + 'planned' end end - end - end - end - end diff --git a/app/presenters/api/v1/language_presenter.rb b/app/presenters/api/v1/language_presenter.rb index c326fc2501..aa017c6dcc 100644 --- a/app/presenters/api/v1/language_presenter.rb +++ b/app/presenters/api/v1/language_presenter.rb @@ -1,91 +1,84 @@ # frozen_string_literal: true module Api - module V1 - + # Helper class for the API V1 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", + 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", + 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", + 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", + da: 'dan', de: 'deu', dv: 'div', dz: 'dzo', - el: "gre", en: "eng", eo: "epo", es: "spa", et: "est", eu: "baq", ee: "ewe", + 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", + 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", + 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", + 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", + ig: 'ibo', io: 'ido', ii: 'iii', iu: 'iku', ie: 'ile', ia: 'ina', id: 'ind', + ik: 'ipk', is: 'ice', it: 'ita', - jv: "jav", ja: "jpn", + 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", + 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", + 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", + 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", + 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", + oc: 'oci', oj: 'oji', or: 'ori', om: 'orm', os: 'oss', - pa: "pan", pi: "pli", pl: "pol", pt: "por", ps: "pus", + pa: 'pan', pi: 'pli', pl: 'pol', pt: 'por', ps: 'pus', - qu: "que", + qu: 'que', - rm: "roh", ro: "rum", rn: "run", ru: "rus", rw: "kin", + 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", + 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", + 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", + ug: 'uig', uk: 'ukr', ur: 'urd', uz: 'uzb', - ve: "ven", vi: "vie", vo: "vol", + ve: 'ven', vi: 'vie', vo: 'vol', - wa: "wln", wo: "wol", + wa: 'wln', wo: 'wol', - xh: "xho", + xh: 'xho', - yi: "yid", yo: "yor", + yi: 'yid', yo: 'yor', - za: "zha", zh: "chi", zu: "zul" + 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:) - two_char_code = lang.to_s.split("-").first + 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/v1/org_presenter.rb b/app/presenters/api/v1/org_presenter.rb index aa430c96fc..5ada0166b1 100644 --- a/app/presenters/api/v1/org_presenter.rb +++ b/app/presenters/api/v1/org_presenter.rb @@ -1,24 +1,17 @@ # frozen_string_literal: true module Api - module V1 - + # Helper class for the API V1 affiliation sections class OrgPresenter - class << self - def affiliation_id(identifiers:) - ident = identifiers.select { |id| id.identifier_scheme&.name == "ror" }.first + ident = identifiers.select { |id| id.identifier_scheme&.name == 'ror' }.first return ident if ident.present? - identifiers.select { |id| id.identifier_scheme&.name == "fundref" }.first + identifiers.select { |id| id.identifier_scheme&.name == 'fundref' }.first end - end - end - end - end diff --git a/app/presenters/api/v1/pagination_presenter.rb b/app/presenters/api/v1/pagination_presenter.rb index 4ffb718c23..5cd07eeebc 100644 --- a/app/presenters/api/v1/pagination_presenter.rb +++ b/app/presenters/api/v1/pagination_presenter.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true module Api - module V1 - + # Helper class for genewric API V1 pagination class PaginationPresenter - def initialize(current_url:, per_page:, total_items:, current_page: 1) @url = current_url @per_page = per_page @@ -16,11 +14,11 @@ def initialize(current_url:, per_page:, total_items:, current_page: 1) 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 = @url.gsub(/per_page=\d+/, '') + .gsub(/page=\d+/, '') + .gsub(/(&)+$/, '').gsub(/\?$/, '') - (url.include?("?") ? "#{url}&" : "#{url}?") + (url.include?('?') ? "#{url}&" : "#{url}?") end def prev_page? @@ -47,9 +45,6 @@ def total_pages (@total_items.to_f / @per_page).ceil end - end - end - end diff --git a/app/presenters/api/v1/plan_presenter.rb b/app/presenters/api/v1/plan_presenter.rb index a8e2510390..291f3bb930 100644 --- a/app/presenters/api/v1/plan_presenter.rb +++ b/app/presenters/api/v1/plan_presenter.rb @@ -1,14 +1,10 @@ # frozen_string_literal: true module Api - module V1 - + # Helper class for the API V1 project / DMP class PlanPresenter - - attr_reader :data_contact - attr_reader :contributors - attr_reader :costs + attr_reader :data_contact, :contributors, :costs def initialize(plan:) @contributors = [] @@ -43,7 +39,7 @@ def identifier # Retrieve the answers that have the Budget theme def plan_costs(plan:) - theme = Theme.where(title: "Cost").first + theme = Theme.where(title: 'Cost').first return [] unless theme.present? # TODO: define a new 'Currency' question type that includes a float field @@ -55,12 +51,9 @@ def plan_costs(plan:) 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 } + currency_code: 'usd', value: answer.text } end end - end - end - end diff --git a/app/presenters/api/v1/research_output_presenter.rb b/app/presenters/api/v1/research_output_presenter.rb new file mode 100644 index 0000000000..28b7325312 --- /dev/null +++ b/app/presenters/api/v1/research_output_presenter.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Api + module V1 + # Helper methods for research outputs + class ResearchOutputPresenter + 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 + + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def fetch_q_and_a(themes:) + return [] unless themes.is_a?(Array) && themes.any? + + ret = themes.map do |theme| + qs = @plan.questions.select { |q| q.themes.collect(&:title).include?(theme) } + descr = qs.map do |q| + a = @plan.answers.select { |ans| ans.question_id = q.id }.first + next unless a.present? && !a.blank? + + "Question: #{q.text}
          Answer: #{a.text}" + end + { title: theme, description: descr } + end + ret.select { |item| item[:description].present? } + end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + end + end +end diff --git a/app/presenters/api/v1/template_presenter.rb b/app/presenters/api/v1/template_presenter.rb index 60870c40d9..50414a349c 100644 --- a/app/presenters/api/v1/template_presenter.rb +++ b/app/presenters/api/v1/template_presenter.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true module Api - module V1 - + # Helper class for the API V1 template info class TemplatePresenter - def initialize(template:) @template = template end @@ -17,9 +15,6 @@ def title "#{@template.title} - with additional questions for #{@template.org.name}" end - end - end - end diff --git a/app/presenters/contributor_presenter.rb b/app/presenters/contributor_presenter.rb index e2b1c03689..e0ea57ee7e 100644 --- a/app/presenters/contributor_presenter.rb +++ b/app/presenters/contributor_presenter.rb @@ -1,29 +1,28 @@ # frozen_string_literal: true +# Helper class for the Contributor pages class ContributorPresenter - class << self - # Returns the name with each word capitalized def display_name(name:) - return "" unless name.present? + return '' unless name.present? - name.split.map(&:capitalize).join(" ") + name.split.map(&:capitalize).join(' ') end # Returns the string name for each role def display_roles(roles:) - return "None" unless roles.present? && roles.any? + return 'None' unless roles.present? && roles.any? - roles.map { |role| role_symbol_to_string(symbol: role) }.join("
          ").html_safe + roles.map { |role| role_symbol_to_string(symbol: role) }.join('
          ').html_safe end # Fetches the contributor's ORCID or initializes one def orcid(contributor:) - orcid = contributor.identifier_for_scheme(scheme: "orcid") + orcid = contributor.identifier_for_scheme(scheme: 'orcid') return orcid if orcid.present? - scheme = IdentifierScheme.by_name("orcid").first + scheme = IdentifierScheme.by_name('orcid').first return nil unless scheme.present? Identifier.new(identifiable: contributor, identifier_scheme: scheme) @@ -40,13 +39,13 @@ def roles_for_radio(contributor:) def role_symbol_to_string(symbol:) case symbol when :data_curation - _("Data Manager") + _('Data Manager') when :project_administration - _("Project Administrator") + _('Project Administrator') when :investigation - _("Principal Investigator") + _('Principal Investigator') else - _("Other") + _('Other') end end @@ -54,17 +53,15 @@ def role_symbol_to_string(symbol:) def role_tooltip(symbol:) case symbol when :data_curation - _("Management activities to annotate (produce metadata), scrub data and maintain research data (including software code, where it is necessary for interpreting the data itself) for initial use and later re-use.") + _('Management activities to annotate (produce metadata), scrub data and maintain research data (including software code, where it is necessary for interpreting the data itself) for initial use and later re-use.') when :investigation - _("Conducting a research and investigation process, specifically performing the experiments, or data/evidence collection.") + _('Conducting a research and investigation process, specifically performing the experiments, or data/evidence collection.') when :project_administration - _("Management and coordination responsibility for the research activity planning and execution.") + _('Management and coordination responsibility for the research activity planning and execution.') else - "" + '' end end # rubocop:enable Layout/LineLength - end - end diff --git a/app/presenters/guidance_presenter.rb b/app/presenters/guidance_presenter.rb index c2acdd6b9a..b268d34935 100644 --- a/app/presenters/guidance_presenter.rb +++ b/app/presenters/guidance_presenter.rb @@ -1,15 +1,15 @@ # frozen_string_literal: true +# Helper class for the guidance pages class GuidancePresenter - - attr_accessor :plan - attr_accessor :guidance_groups + attr_accessor :plan, :guidance_groups def initialize(plan) @plan = plan @guidance_groups = plan.guidance_groups.where(published: true) end + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def any?(org: nil, question: nil) if org.nil? return hashified_annotations? || hashified_guidance_groups? unless question.present? @@ -30,7 +30,7 @@ def any?(org: nil, question: nil) guidance_annotations?(org: org, question: question) || guidance_groups_by_theme?(org: org, question: question) end - # rubocop:enable + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # filters through the orgs with annotations and guidance groups to create a # set of tabs with display names and any guidance/annotations to show @@ -38,6 +38,7 @@ def any?(org: nil, question: nil) # question - The question to which guidance pretains # # Returns an array of tab hashes. These + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize def tablist(question) # start with orgs # filter into hash with annotation_presence, main_group presence, and @@ -60,6 +61,7 @@ def tablist(question) end display_tabs end + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize private @@ -105,21 +107,22 @@ def guidance_annotations?(org: nil, question: nil) return false unless hashified_annotations.key?(org) hashified_annotations[org].find do |annotation| - (annotation.question_id == question.id) && (annotation.type == "guidance") + (annotation.question_id == question.id) && (annotation.type == 'guidance') end.present? end # Returns a hash of guidance groups for an org and question passed with the following # structure: # { guidance_group: { theme: [guidance, ...], ... }, ... } + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize def guidance_groups_by_theme(org: nil, question: nil) raise ArgumentError unless question.is_a?(Question) raise ArgumentError unless org.is_a?(Org) return {} unless hashified_guidance_groups.key?(org) - hashified_guidance_groups[org].each_key.each_with_object({}) do |gg, acc| - filtered_gg = hashified_guidance_groups[org][gg].each_key.each_with_object({}) do |theme, ac| + hashified_guidance_groups[org].each_key.with_object({}) do |gg, acc| + filtered_gg = hashified_guidance_groups[org][gg].each_key.with_object({}) do |theme, ac| next unless question.themes.include?(theme) ac[theme] = hashified_guidance_groups[org][gg][theme] @@ -127,6 +130,7 @@ def guidance_groups_by_theme(org: nil, question: nil) acc[gg] = filtered_gg if filtered_gg.present? end end + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize # Returns a collection of annotations (type guidance) for an org and question passed def guidance_annotations(org: nil, question: nil) @@ -134,7 +138,7 @@ def guidance_annotations(org: nil, question: nil) return [] unless hashified_annotations.key?(org) hashified_annotations[org].select do |annotation| - (annotation.question_id == question.id) && (annotation.type == "guidance") + (annotation.question_id == question.id) && (annotation.type == 'guidance') end end @@ -143,7 +147,7 @@ def orgs_from_guidance_groups @orgs_from_guidance_groups = Org.joins(:guidance_groups) .where(guidance_groups: { id: guidance_groups.ids }) - .distinct("orgs.id") + .distinct('orgs.id') @orgs_from_guidance_groups end @@ -224,5 +228,4 @@ def hashify_annotations acc[org] = annotations.select { |annotation| annotation.org_id = org.id } end end - end diff --git a/app/presenters/identifier_presenter.rb b/app/presenters/identifier_presenter.rb index f01845455f..77e66d6bbe 100644 --- a/app/presenters/identifier_presenter.rb +++ b/app/presenters/identifier_presenter.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true +# Helper class for displaying identifiers class IdentifierPresenter - - attr_reader :schemes - attr_reader :identifiable + attr_reader :schemes, :identifiable def initialize(identifiable:) @identifiable = identifiable @@ -25,6 +24,8 @@ def scheme_by_name(name:) private + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def load_schemes # Load the schemes for the current context schemes = IdentifierScheme.for_orgs if @identifiable.is_a?(Org) @@ -38,9 +39,10 @@ def load_schemes # a curated list of Orgs that can use institutional login if @identifiable.is_a?(Org) && !Rails.configuration.x.shibboleth.use_filtered_discovery_service - schemes = schemes.reject { |scheme| scheme.name.downcase == "shibboleth" } + schemes = schemes.reject { |scheme| scheme.name.downcase == 'shibboleth' } end schemes end - + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity end diff --git a/app/presenters/org_selection_presenter.rb b/app/presenters/org_selection_presenter.rb index a362d022b3..2d8820eecc 100644 --- a/app/presenters/org_selection_presenter.rb +++ b/app/presenters/org_selection_presenter.rb @@ -1,17 +1,15 @@ # frozen_string_literal: true +# Helper class for the Org selection typeahead class OrgSelectionPresenter - attr_accessor :suggestions def initialize(orgs:, selection:) @crosswalk = [] - @name = selection.present? ? selection.name : "" + @name = selection.present? ? selection.name : '' - if selection.present? - orgs = [selection] if !orgs.present? || orgs.empty? - end + orgs = [selection] if !orgs.present? || orgs.empty? @crosswalk = orgs.map do |org| next if org.nil? @@ -40,5 +38,4 @@ def crosswalk_entry_from_org_id(value:) rescue StandardError {}.to_json end - end diff --git a/app/presenters/plan_presenter.rb b/app/presenters/plan_presenter.rb index 1d24a9148e..0c34fb8859 100644 --- a/app/presenters/plan_presenter.rb +++ b/app/presenters/plan_presenter.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Helper class for plans class PlanPresenter - attr_accessor :plan def initialize(plan) @@ -17,7 +17,6 @@ def project_dates_to_readonly_display return "Starts on #{sd}" if sd.present? return "Ends on #{ed}" if ed.present? - "" + '' end - end diff --git a/app/presenters/research_output_presenter.rb b/app/presenters/research_output_presenter.rb new file mode 100644 index 0000000000..3a77f1be80 --- /dev/null +++ b/app/presenters/research_output_presenter.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +# Helper methods for the research outputs tab +class ResearchOutputPresenter + attr_accessor :research_output + + def initialize(research_output:) + @research_output = research_output + end + + # Returns the output_type list for a select_tag + def selectable_output_types + ResearchOutput.output_types + .map { |k, _v| [k.humanize, k] } + end + + # Returns the access options for a select tag + def selectable_access_types + ResearchOutput.accesses + .map { |k, _v| [k.humanize, k] } + end + + # Returns the options for file size units + def selectable_size_units + [%w[MB mb], %w[GB gb], %w[TB tb], %w[PB pb], ['bytes', '']] + end + + # Returns the options for metadata standards + def selectable_metadata_standards(category:) + out = MetadataStandard.all.order(:title).map { |ms| [ms.title, ms.id] } + return out unless category.present? + + MetadataStandard.where(descipline_specific: (category == 'disciplinary')) + .map { |ms| [ms.title, ms.id] } + end + + # Returns the available licenses for a select tag + def complete_licenses + License.selectable + .sort { |a, b| a.identifier <=> b.identifier } + .map { |license| [license.identifier, license.id] } + end + + # Returns the available licenses for a select tag + def preferred_licenses + License.preferred.map { |license| [license.identifier, license.id] } + end + + # Returns whether or not we should capture the byte_size based on the output_type + def byte_sizable? + @research_output.audiovisual? || @research_output.sound? || @research_output.image? || + @research_output.model_representation? || + @research_output.data_paper? || @research_output.dataset? || @research_output.text? + end + + # Returns the options for subjects for the repository filter + def self.selectable_subjects + [ + '23-Agriculture, Forestry, Horticulture and Veterinary Medicine', + '21-Biology', + '31-Chemistry', + '44-Computer Science, Electrical and System Engineering', + '45-Construction Engineering and Architecture', + '34-Geosciences (including Geography)', + '11-Humanities', + '43-Materials Science and Engineering', + '33-Mathematics', + '41-Mechanical and industrial Engineering', + '22-Medicine', + '32-Physics', + '12-Social and Behavioural Sciences', + '42-Thermal Engineering/Process Engineering' + ].map do |subject| + [subject.split('-').last, subject.gsub('-', ' ')] + end + end + + # Returns the options for the repository type + def self.selectable_repository_types + [ + [_('Generalist (multidisciplinary)'), 'other'], + [_('Discipline specific'), 'disciplinary'], + [_('Institutional'), 'institutional'] + ] + end + + # Converts the byte_size into a more friendly value (e.g. 15.4 MB) + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity + def converted_file_size(size:) + return { size: nil, unit: 'mb' } unless size.present? && size.is_a?(Numeric) && size.positive? + return { size: size / 1.petabytes, unit: 'pb' } if size >= 1.petabytes + return { size: size / 1.terabytes, unit: 'tb' } if size >= 1.terabytes + return { size: size / 1.gigabytes, unit: 'gb' } if size >= 1.gigabytes + return { size: size / 1.megabytes, unit: 'mb' } if size >= 1.megabytes + + { size: size, unit: '' } + end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity + + # Returns the truncated title if it is greater than 50 characters + def display_name + return '' unless @research_output.is_a?(ResearchOutput) + return "#{@research_output.title[0..49]} ..." if @research_output.title.length > 50 + + @research_output.title + end + + # Returns the humanized version of the output_type enum variable + def display_type + return '' unless @research_output.is_a?(ResearchOutput) + # Return the user entered text for the type if they selected 'other' + return @research_output.output_type_description if @research_output.other? + + @research_output.output_type.gsub('_', ' ').capitalize + end + + # Returns the display name(s) of the repository(ies) + def display_repository + return [_('None specified')] unless @research_output.repositories.any? + + @research_output.repositories.map(&:name) + end + + # Returns the display the license name + def display_license + return _('None specified') unless @research_output.license.present? + + @research_output.license.name + end + + # Returns the display name(s) of the repository(ies) + def display_metadata_standard + return [_('None specified')] unless @research_output.metadata_standards.any? + + @research_output.metadata_standards.map(&:title) + end + + # Returns the humanized version of the access enum variable + def display_access + return _('Unspecified') unless @research_output.access.present? + + @research_output.access.capitalize + end + + # Returns the release date as a date + def display_release + return _('Unspecified') unless @research_output.release_date.present? + + @research_output.release_date.to_date + end + + # Return 'Yes', 'No' or 'Unspecified' depending on the value + def display_boolean(value:) + return 'Unspecified' if value.nil? + + value ? 'Yes' : 'No' + end +end diff --git a/app/presenters/super_admin/orgs/merge_presenter.rb b/app/presenters/super_admin/orgs/merge_presenter.rb index 0e503408e7..7adad112c3 100644 --- a/app/presenters/super_admin/orgs/merge_presenter.rb +++ b/app/presenters/super_admin/orgs/merge_presenter.rb @@ -1,23 +1,22 @@ # frozen_string_literal: true module SuperAdmin - module Orgs - + # Helper class for the analysis that the Super Admin sees before actually merging orgs class MergePresenter - attr_accessor :from_org, :to_org, :from_org_name, :to_org_name, :from_org_entries, :to_org_entries, :mergeable_entries, :categories, :from_org_attributes, :to_org_attributes, :mergeable_attributes + # rubocop:disable Metrics/AbcSize def initialize(from_org:, to_org:) @from_org = from_org @to_org = to_org # Abbreviated Org names for display in tables - @from_org_name = @from_org.name.split(" ")[0..2].join(" ") - @to_org_name = @to_org.name.split(" ")[0..2].join(" ") + @from_org_name = @from_org.name.split[0..2].join(' ') + @to_org_name = @to_org.name.split[0..2].join(' ') # Association records @from_org_entries = prepare_org(org: @from_org) @@ -30,10 +29,12 @@ def initialize(from_org:, to_org:) @to_org_attributes = org_attributes(org: @to_org) @mergeable_attributes = mergeable_columns end + # rubocop:enable Metrics/AbcSize private # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def prepare_org(org:) return {} unless org.present? && org.is_a?(Org) @@ -54,6 +55,7 @@ def prepare_org(org:) } end # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def prepare_mergeables return {} unless @from_org_entries.any? && @to_org_entries.any? @@ -73,6 +75,7 @@ def prepare_mergeables end # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def diff_from_and_to(category:) return [] unless category.present? && @from_org_entries.fetch(category, []).any? @@ -101,6 +104,7 @@ def diff_from_and_to(category:) end end # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def org_attributes(org:) return {} unless org.is_a?(Org) @@ -118,6 +122,7 @@ def org_attributes(org:) } end + # rubocop:disable Metrics/AbcSize def mergeable_columns out = {} out[:target_url] = @from_org.target_url if mergeable_column?(column: :target_url) @@ -138,12 +143,15 @@ def mergeable_columns end out end + # rubocop:enable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def mergeable_column?(column:) case column when :links - (@to_org.links.nil? || @to_org.links.fetch("org", []).empty?) && - (@from_org.links.present? || @from_org.links.fetch("org", [].any?)) + (@to_org.links.nil? || @to_org.links.fetch('org', []).empty?) && + (@from_org.links.present? || @from_org.links.fetch('org', [].any?)) when :managed !@to_org.managed? && @from_org.managed? when :feedback_enabled @@ -154,9 +162,8 @@ def mergeable_column?(column:) @to_org != @from_org end end - + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity end - end - end diff --git a/app/scrubbers/table_free_scrubber.rb b/app/scrubbers/table_free_scrubber.rb index 4b5f87d8e2..ad2dd77482 100644 --- a/app/scrubbers/table_free_scrubber.rb +++ b/app/scrubbers/table_free_scrubber.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Logic that ensures that table tags are allowed from TinyMCE editor results class TableFreeScrubber < Rails::Html::PermitScrubber - TABLE_TAGS = %w[table thead tbody tr td th tfoot caption].freeze ALLOWED_TAGS = Rails.application.config.action_view.sanitized_allowed_tags - TABLE_TAGS @@ -10,5 +10,4 @@ def initialize super self.tags = ALLOWED_TAGS end - end diff --git a/app/services/api/v1/auth/jwt/authentication_service.rb b/app/services/api/v1/auth/jwt/authentication_service.rb index 59d03dc1c5..8bb0ab765b 100644 --- a/app/services/api/v1/auth/jwt/authentication_service.rb +++ b/app/services/api/v1/auth/jwt/authentication_service.rb @@ -1,13 +1,9 @@ # frozen_string_literal: true module Api - module V1 - module Auth - module Jwt - # This class provides Authentication for: # # ApiClients (aka machines) with the following JSON body: { @@ -23,21 +19,19 @@ module Jwt # } # class AuthenticationService - - attr_reader :errors - attr_reader :expiration + attr_reader :errors, :expiration def initialize(json: {}) json = json.nil? ? {} : json.with_indifferent_access - type = json.fetch(:grant_type, "client_credentials") - parse_client(json: json) if type == "client_credentials" - parse_code(json: json) if type == "authorization_code" + type = json.fetch(:grant_type, 'client_credentials') + parse_client(json: json) if type == 'client_credentials' + parse_code(json: json) if type == 'authorization_code' @errors = {} if @client_id.nil? || @client_secret.nil? || !%w[client_credentials authorization_code].include?(type) - @errors[:client_authentication] = _("Invalid grant type") + @errors[:client_authentication] = _('Invalid grant type') end end @@ -63,10 +57,7 @@ def call private - attr_reader :client_id - attr_reader :client_secret - attr_reader :api_client - attr_reader :auth_method + attr_reader :client_id, :client_secret, :api_client, :auth_method # Returns the matching ApiClient if authentication succeeds def client @@ -76,7 +67,7 @@ def client return @api_client if @api_client.present? # Record an error if no ApiClient or User was authenticated - @errors[:client_authentication] = _("Invalid credentials") + @errors[:client_authentication] = _('Invalid credentials') nil end @@ -93,7 +84,7 @@ def authenticate_client # Tries to find a User whose email matches the :client_id. If found # it will attempt to authenticate the :api_token against the :client_secret def authenticate_user - users = User.where("lower(email) LIKE lower(?)", @client_id) + users = User.where('lower(email) LIKE lower(?)', @client_id) return nil unless users.present? && users.any? usr = users.first @@ -106,22 +97,17 @@ def authenticate_user def parse_client(json: {}) @client_id = json.fetch(:client_id, nil) @client_secret = json.fetch(:client_secret, nil) - @auth_method = "authenticate_client" + @auth_method = 'authenticate_client' end # Handles User credentials def parse_code(json: {}) @client_id = json.fetch(:email, nil) @client_secret = json.fetch(:code, nil) - @auth_method = "authenticate_user" + @auth_method = 'authenticate_user' end - end - end - end - end - end diff --git a/app/services/api/v1/auth/jwt/authorization_service.rb b/app/services/api/v1/auth/jwt/authorization_service.rb index bb9450d904..52caea688e 100644 --- a/app/services/api/v1/auth/jwt/authorization_service.rb +++ b/app/services/api/v1/auth/jwt/authorization_service.rb @@ -1,15 +1,11 @@ # frozen_string_literal: true module Api - module V1 - module Auth - module Jwt - + # Class to handle User authorization class AuthorizationService - def initialize(headers: {}) @headers = headers.nil? ? {} : headers @errors = ActiveSupport::HashWithIndifferentAccess.new @@ -24,12 +20,13 @@ def call private # Lookup the Client bassed on the client_id embedded in the JWT + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity def client return @api_client if @api_client.present? token = decoded_auth_token # If the token is missing or invalid then set the client to nil - errors[:token] = _("Invalid token") unless token.present? + errors[:token] = _('Invalid token') unless token.present? @api_client = nil unless token.present? && token[:client_id].present? return @api_client unless token.present? && token[:client_id].present? @@ -38,7 +35,7 @@ def client @api_client = User.where(email: token[:client_id]).first end - # rubocop:enable + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity def decoded_auth_token return @token if @token.present? @@ -46,25 +43,20 @@ def decoded_auth_token @token = JsonWebToken.decode(token: http_auth_header) @token rescue JWT::ExpiredSignature - errors[:token] = _("Token expired") + errors[:token] = _('Token expired') nil end # Extract the token from the Authorization header def http_auth_header hdr = @headers[:Authorization] - errors[:token] = _("Missing token") unless hdr.present? + errors[:token] = _('Missing token') unless hdr.present? return nil unless hdr.present? hdr.split.last end - end - end - end - end - end diff --git a/app/services/api/v1/auth/jwt/json_web_token.rb b/app/services/api/v1/auth/jwt/json_web_token.rb index 2a60c2add1..4bd5c850a2 100644 --- a/app/services/api/v1/auth/jwt/json_web_token.rb +++ b/app/services/api/v1/auth/jwt/json_web_token.rb @@ -1,20 +1,15 @@ # frozen_string_literal: true module Api - module V1 - module Auth - module Jwt - + # Class to handle encryption/descryption of the JWT class JsonWebToken - class << self - def encode(payload:, exp: 24.hours.from_now) payload[:exp] = exp.to_i - # JWT.encode(payload, Rails.application.credentials.secret_key_base) + # JWT.encode(payload, Rails.application.credentials.secret_key_base) # We are not using credential JWT.encode(payload, Rails.application.secrets.secret_key_base) rescue JWT::EncodeError => e Rails.logger.error "Api::V1::Auth::Jwt::JsonWebToken.encode - #{e.message}" @@ -31,15 +26,9 @@ def decode(token:) Rails.logger.error "Api::V1::Auth::Jwt::JsonWebToken.decode - #{e.message}" nil end - end - end - end - end - end - end diff --git a/app/services/api/v1/contextual_error_service.rb b/app/services/api/v1/contextual_error_service.rb index 7dc67ad464..b9ad32d761 100644 --- a/app/services/api/v1/contextual_error_service.rb +++ b/app/services/api/v1/contextual_error_service.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module Api - module V1 - # Service that takes standard ActiveModel errors and contextualizes them # so that they relate to the structure of the JSON format used by the API # to help the caller understand the errors. @@ -12,9 +10,7 @@ module V1 # "Name can't be blank" becomes "Funder name can't be blank" # "Contributors org name can't ..." becomes "Contact/Contributor affiliation name can't ..." class ContextualErrorService - class << self - # Process the plan's errors and any of its associations # rubocop:disable Metrics/AbcSize def process_plan_errors(plan:) @@ -24,31 +20,33 @@ def process_plan_errors(plan:) return errs unless plan.funder.present? || plan.grant.present? plan.funder.valid? - errs << contextualize(errors: plan.funder.errors, context: "Funding") + errs << contextualize(errors: plan.funder.errors, context: 'Funding') return errs unless plan.grant.present? plan.grant.valid? - errs << contextualize(errors: plan.grant.errors, context: "Grant") + errs << contextualize(errors: plan.grant.errors, context: 'Grant') errs.flatten.compact.uniq end # rubocop:enable Metrics/AbcSize # Add context to the standard error message - def contextualize(errors:, context: "DMP") + # rubocop:disable Metrics/AbcSize + def contextualize(errors:, context: 'DMP') errs = errors.is_a?(ActiveModel::Errors) ? errors.full_messages : [] errs = errors if errors.is_a?(Array) && errs.empty? return errs unless errs.any? errs.map do |msg| - msg.gsub("org name", "affiliation name") - .gsub("identifiers value", "identifier values") - .gsub("Contributors", "Contact/Contributor") + msg.gsub('org name', 'affiliation name') + .gsub('identifiers value', 'identifier values') + .gsub('Contributors', 'Contact/Contributor') .gsub(/^Identifiers/, "#{context.capitalize} identifier") .gsub(/^Name/, "#{context.capitalize} name") .gsub(/^Title/, "#{context.capitalize} title") .gsub(/^Value/, "#{context.capitalize} value") end end + # rubocop:enable Metrics/AbcSize # Checks the plan and optional associations for validity def valid_plan?(plan:) @@ -56,11 +54,7 @@ def valid_plan?(plan:) (plan.funder.blank? || plan.funder.valid?) && (plan.grant.blank? || plan.grant.value.present?) end - end - end - end - end diff --git a/app/services/api/v1/conversion_service.rb b/app/services/api/v1/conversion_service.rb index 9b9af2c270..d024ccc926 100644 --- a/app/services/api/v1/conversion_service.rb +++ b/app/services/api/v1/conversion_service.rb @@ -1,27 +1,24 @@ # frozen_string_literal: true module Api - module V1 - + # 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 'yes' if [true, 1].include?(value) - return "no" if [false, 0].include?(value) + return 'no' if [false, 0].include?(value) - "unknown" + '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 true if value&.downcase == 'yes' - return nil if value.blank? || value&.downcase == "unknown" + return nil if value.blank? || value&.downcase == 'unknown' false end @@ -35,11 +32,7 @@ def to_identifier(context:, value:) scheme = IdentifierScheme.new(name: context) Identifier.new(value: value, identifier_scheme: scheme) end - end - end - end - end diff --git a/app/services/api/v1/deserialization/contributor.rb b/app/services/api/v1/deserialization/contributor.rb index 9c6f5dc75d..79a2eff48e 100644 --- a/app/services/api/v1/deserialization/contributor.rb +++ b/app/services/api/v1/deserialization/contributor.rb @@ -1,15 +1,11 @@ # frozen_string_literal: true module Api - module V1 - module Deserialization - + # Logic to deserialize RDA common standard to a Contributor object class Contributor - class << self - # Convert the incoming JSON into a Contributor # { # "role": [ @@ -67,13 +63,13 @@ def find_or_initialize(id_json:, json: {}) return nil unless json.present? contrib = Api::V1::DeserializationService.object_from_identifier( - class_name: "Contributor", json: id_json + class_name: 'Contributor', json: id_json ) return duplicate_contributor(contributor: contrib) if contrib.present? if json[:mbox].present? # Try to find by email - contrib = ::Contributor.where("LOWER(email) = ?", json[:mbox]&.downcase).last + contrib = ::Contributor.where('LOWER(email) = ?', json[:mbox]&.downcase).last return duplicate_contributor(contributor: contrib) if contrib.present? end @@ -106,13 +102,8 @@ def assign_roles(contributor:, json: {}) end contributor end - end - end - end - end - end diff --git a/app/services/api/v1/deserialization/dataset.rb b/app/services/api/v1/deserialization/dataset.rb index 37762c1fa2..53220636db 100644 --- a/app/services/api/v1/deserialization/dataset.rb +++ b/app/services/api/v1/deserialization/dataset.rb @@ -1,15 +1,11 @@ # frozen_string_literal: true module Api - module V1 - module Deserialization - + # Logic to deserialize RDA common standard to a ResearchOutput object class Dataset - class << self - # Convert incoming JSON into a Dataset # { # "title": "Cerebral cortex imaging series", @@ -35,13 +31,8 @@ def deserialize(json: {}) # TODO: Implement once we have determined the Dataset model nil end - end - end - end - end - end diff --git a/app/services/api/v1/deserialization/funding.rb b/app/services/api/v1/deserialization/funding.rb index ea4d8c4d47..563690e562 100644 --- a/app/services/api/v1/deserialization/funding.rb +++ b/app/services/api/v1/deserialization/funding.rb @@ -1,15 +1,11 @@ # frozen_string_literal: true module Api - module V1 - module Deserialization - + # Logic to deserialize RDA common standard to a Plan funder and grant infor class Funding - class << self - # Convert the funding information and attach to the Plan # { # "$ref": "SEE Org.deserialize! for details", @@ -33,13 +29,8 @@ def deserialize(plan:, json: {}) ) plan end - end - end - end - end - end diff --git a/app/services/api/v1/deserialization/identifier.rb b/app/services/api/v1/deserialization/identifier.rb index 8053547f87..5cbf8a44c7 100644 --- a/app/services/api/v1/deserialization/identifier.rb +++ b/app/services/api/v1/deserialization/identifier.rb @@ -1,15 +1,11 @@ # frozen_string_literal: true module Api - module V1 - module Deserialization - + # Logic to deserialize RDA common standard to an Identifier object class Identifier - class << self - # Convert the incoming JSON into an Identifier # { # "type": "ror", @@ -29,13 +25,8 @@ def deserialize(class_name:, json: {}) ::Identifier.new(identifier_scheme: scheme, value: json[:identifier]) end - end - end - end - end - end diff --git a/app/services/api/v1/deserialization/org.rb b/app/services/api/v1/deserialization/org.rb index 1adbdb9f8b..65773951b6 100644 --- a/app/services/api/v1/deserialization/org.rb +++ b/app/services/api/v1/deserialization/org.rb @@ -1,15 +1,11 @@ # frozen_string_literal: true module Api - module V1 - module Deserialization - + # Logic to deserialize RDA common standard to a Org object class Org - class << self - # Convert the incoming JSON into an Org # { # "name": "University of Somewhere", @@ -19,6 +15,7 @@ class << self # "identifier": "https://ror.org/43y4g4" # } # } + # rubocop:disable Metrics/AbcSize def deserialize(json: {}) return nil unless Api::V1::JsonValidationService.org_valid?(json: json) @@ -27,7 +24,7 @@ def deserialize(json: {}) # Try to find the Org by the identifier id_json = json.fetch(:affiliation_id, json.fetch(:funder_id, {})) org = Api::V1::DeserializationService.object_from_identifier( - class_name: "Org", json: id_json + class_name: 'Org', json: id_json ) # Try to find the Org by name @@ -42,6 +39,7 @@ def deserialize(json: {}) # Attach the identifier Api::V1::DeserializationService.attach_identifier(object: org, json: id_json) end + # rubocop:enable Metrics/AbcSize # =================== # = PRIVATE METHODS = @@ -56,7 +54,7 @@ def find_by_name(json: {}) name = json[:name] # Search the DB - org = ::Org.where("LOWER(name) = ?", name.downcase).first + org = ::Org.where('LOWER(name) = ?', name.downcase).first return org if org.present? # External ROR search @@ -72,13 +70,8 @@ def find_by_name(json: {}) result ||= { name: name } OrgSelection::HashToOrgService.to_org(hash: result) end - end - end - end - end - end diff --git a/app/services/api/v1/deserialization/plan.rb b/app/services/api/v1/deserialization/plan.rb index 5fbd2101bc..13a118754a 100644 --- a/app/services/api/v1/deserialization/plan.rb +++ b/app/services/api/v1/deserialization/plan.rb @@ -1,15 +1,11 @@ # frozen_string_literal: true module Api - module V1 - module Deserialization - + # Logic to deserialize RDA common standard to a Plan object class Plan - class << self - # Convert the incoming JSON into a Plan # { # "dmp": { @@ -48,6 +44,7 @@ class << self # }] # } # } + # rubocop:disable Metrics/AbcSize def deserialize(json: {}) return nil unless Api::V1::JsonValidationService.plan_valid?(json: json) @@ -73,6 +70,7 @@ def deserialize(json: {}) plan = deserialize_contributors(plan: plan, json: json) deserialize_datasets(plan: plan, json: json) end + # rubocop:enable Metrics/AbcSize # =================== # = PRIVATE METHODS = @@ -88,12 +86,12 @@ def find_or_initialize(id_json:, json: {}) if Api::V1::DeserializationService.doi?(value: id) # Find by the DOI or ARK plan = Api::V1::DeserializationService.object_from_identifier( - class_name: "Plan", json: id_json + class_name: 'Plan', json: id_json ) else # For URL based identifiers begin - plan = ::Plan.find_by(id: id.split("/").last.to_i) + plan = ::Plan.find_by(id: id.split('/').last.to_i) rescue StandardError # Catches scenarios where the dmp_id is NOT one of our URLs plan = nil @@ -115,6 +113,7 @@ def deserialize_datasets(plan:, json: {}) end # Deserialize the project information and attach to Plan + # rubocop:disable Metrics/AbcSize def deserialize_project(plan:, json: {}) return plan unless json.present? && json[:project].present? && @@ -130,7 +129,7 @@ def deserialize_project(plan:, json: {}) Api::V1::Deserialization::Funding.deserialize(plan: plan, json: funding) end - # rubocop:enable + # rubocop:enable Metrics/AbcSize # Deserialize the contact as a Contributor def deserialize_contact(plan:, json: {}) @@ -172,13 +171,8 @@ def template_id(json: {}) extensions.fetch(:template, {})[:id] end - end - end - end - end - end diff --git a/app/services/api/v1/deserialization_service.rb b/app/services/api/v1/deserialization_service.rb index 705e0c6d98..383cab77ad 100644 --- a/app/services/api/v1/deserialization_service.rb +++ b/app/services/api/v1/deserialization_service.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true module Api - module V1 - + # Generic deserialization helper methods class DeserializationService - class << self - # Finds the object by the specified identifier def object_from_identifier(class_name:, json:) return nil unless class_name.present? && json.present? && @@ -24,6 +21,7 @@ def object_from_identifier(class_name:, json:) end # Attach the identifier to the object if it does not already exist + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def attach_identifier(object:, json:) return object unless object.present? && object.respond_to?(:identifiers) && json.present? && @@ -39,6 +37,7 @@ def attach_identifier(object:, json:) ) object end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # Translates the role in the json to a Contributor role def translate_role(role:) @@ -49,7 +48,7 @@ def translate_role(role:) # Strip off the URL if present url = ::Contributor::ONTOLOGY_BASE_URL - role = role.gsub("#{url}/", "").downcase if role.include?(url) + role = role.gsub("#{url}/", '').downcase if role.include?(url) # Return the role if its a valid one otherwise defualt return role if ::Contributor.new.all_roles.include?(role.downcase.to_sym) @@ -59,14 +58,12 @@ def translate_role(role:) # Retrieve any JSON schema extensions for this application def app_extensions(json: {}) - return {} unless json.present? && json[:extension].present? - app = I18n.with_locale I18n.default_locale do - ::ApplicationService.application_name - end - ext = json[:extension].select { |item| item[app.to_sym].present? } - ext.first.present? ? ext.first[app.to_sym] : {} + # Note the symbol of the dmproadmap json object + # nested in extensions which is the container for the json template object, etc. + ext = json[:extension].select { |item| item[:dmproadmap].present? } + ext.first.present? ? ext.first[:dmproadmap] : {} end # Determines whether or not the value is a DOI/ARK @@ -76,7 +73,7 @@ def doi?(value:) # The format must match a DOI or ARK and a DOI IdentifierScheme # must also be present! identifier = ::Identifier.new(value: value) - scheme = ::IdentifierScheme.find_by(name: "doi") + scheme = ::IdentifierScheme.find_by(name: 'doi') %w[ark doi].include?(identifier.identifier_format) && scheme.present? end @@ -88,11 +85,7 @@ def safe_date(value:) rescue ArgumentError value.to_s end - end - end - end - end diff --git a/app/services/api/v1/json_validation_service.rb b/app/services/api/v1/json_validation_service.rb index 6adb24c8fd..8b716e5a48 100644 --- a/app/services/api/v1/json_validation_service.rb +++ b/app/services/api/v1/json_validation_service.rb @@ -1,23 +1,16 @@ # frozen_string_literal: true module Api - module V1 - # Service used to validate incoming JSON class JsonValidationService - - # rubocop:disable Layout/LineLength BAD_PLAN_MSG = _(":title and the contact's :mbox are both required fields").freeze - BAD_ID_MSG = _(":type and :identifier are required for all ids").freeze - BAD_ORG_MSG = _(":name is required for every :affiliation and :funding").freeze - BAD_CONTRIB_MSG = _(":role and either the :name or :email are required for each :contributor").freeze - BAD_FUNDING_MSG = _(":name, :funder_id or :grant_id are required for each funding").freeze - BAD_DATASET_MSSG = _(":title is required for each :dataset").freeze - # rubocop:enable Layout/LineLength - + BAD_ID_MSG = _(':type and :identifier are required for all ids').freeze + BAD_ORG_MSG = _(':name is required for every :affiliation and :funding').freeze + BAD_CONTRIB_MSG = _(':role and either the :name or :email are required for each :contributor').freeze + BAD_FUNDING_MSG = _(':name, :funder_id or :grant_id are required for each funding').freeze + BAD_DATASET_MSSG = _(':title is required for each :dataset').freeze class << self - def plan_valid?(json:) json.present? && json[:title].present? && json[:contact].present? && json[:contact][:mbox].present? @@ -54,14 +47,13 @@ def dataset_valid?(json:) # rubocop:disable Metrics/AbcSize # Scans the entire JSON document for invalid metadata and returns # friendly errors to help the caller resolve the issue + # rubocop:disable Metrics/CyclomaticComplexity def validation_errors(json:) errs = [] - return [_("invalid JSON")] unless json.present? + return [_('invalid JSON')] unless json.present? errs << BAD_PLAN_MSG unless plan_valid?(json: json) - if json[:dmp_id].present? - errs << BAD_ID_MSG unless identifier_valid?(json: json[:dmp_id]) - end + errs << BAD_ID_MSG if json[:dmp_id].present? && !identifier_valid?(json: json[:dmp_id]) # Handle Contact errs << contributor_validation_errors(json: json[:contact]) @@ -70,6 +62,7 @@ def validation_errors(json:) errs << json.fetch(:contributor, []).map do |contributor| contributor_validation_errors(json: contributor) end + # rubocop:enable Metrics/CyclomaticComplexity # Handle the Project and Fundings json.fetch(:project, []).each do |project| @@ -93,9 +86,7 @@ def contributor_validation_errors(json:) is_contact: true) errs << org_validation_errors(json: json[:affiliation]) if json[:affiliation].present? id = json.fetch(:contributor_id, json[:contact_id]) - if id.present? - errs << BAD_ID_MSG unless identifier_valid?(json: id) - end + errs << BAD_ID_MSG if id.present? && !identifier_valid?(json: id) end errs end @@ -106,9 +97,7 @@ def funding_validation_errors(json:) errs << BAD_FUNDING_MSG unless funding_valid?(json: json) errs << org_validation_errors(json: json) - if json[:grant_id].present? - errs << BAD_ID_MSG unless identifier_valid?(json: json[:grant_id]) - end + errs << BAD_ID_MSG if json[:grant_id].present? && !identifier_valid?(json: json[:grant_id]) errs end @@ -118,16 +107,10 @@ def org_validation_errors(json:) errs << BAD_ORG_MSG unless org_valid?(json: json) id = json.fetch(:affiliation_id, json[:funder_id]) - if id.present? - errs << BAD_ID_MSG unless identifier_valid?(json: id) - end + errs << BAD_ID_MSG if id.present? && !identifier_valid?(json: id) errs end - - end - +end end - end - end diff --git a/app/services/api/v1/persistence_service.rb b/app/services/api/v1/persistence_service.rb index 1de5403129..3a92230256 100644 --- a/app/services/api/v1/persistence_service.rb +++ b/app/services/api/v1/persistence_service.rb @@ -1,14 +1,11 @@ # frozen_string_literal: true module Api - module V1 - # Service used to ensure the entire DMP stack is saved class PersistenceService - class << self - + # rubocop:disable Metrics/AbcSize def safe_save(plan:) return nil unless plan.is_a?(Plan) && plan.valid? @@ -32,6 +29,7 @@ def safe_save(plan:) plan.reload end end + # rubocop:enable Metrics/AbcSize private @@ -67,6 +65,7 @@ def safe_save_org(org:) end end + # rubocop:disable Metrics/AbcSize def safe_save_contributor(contributor:) return nil unless contributor.is_a?(Contributor) && contributor.valid? @@ -85,9 +84,11 @@ def safe_save_contributor(contributor:) contrib.reload end end + # rubocop:enable Metrics/AbcSize # Consolidate the contributors so that we don't end up trying to insert # duplicate records! + # rubocop:disable Metrics/CyclomaticComplexity def deduplicate_contributors(contributors:) out = [] return out unless contributors.respond_to?(:any?) && contributors.any? @@ -104,6 +105,7 @@ def deduplicate_contributors(contributors:) end out.flatten.compact.uniq end + # rubocop:enable Metrics/CyclomaticComplexity def id_for(model, scheme) return nil unless model.respond_to?(:identifier_for_scheme) && scheme.present? @@ -115,11 +117,7 @@ def saveable_attributes(attrs:) %w[id created_at updated_at].each { |key| attrs.delete(key) } attrs end - end - end - end - end diff --git a/app/services/api/v1/validation_service.rb b/app/services/api/v1/validation_service.rb index b06a72e5b0..83aefb40c6 100644 --- a/app/services/api/v1/validation_service.rb +++ b/app/services/api/v1/validation_service.rb @@ -1,14 +1,10 @@ # frozen_string_literal: true module Api - module V1 - # Service used to validate incoming JSON class ValidationService - class << self - def plan_valid?(json:) json.present? && json[:title].present? && json[:contact].present? && json[:contact][:mbox].present? @@ -45,11 +41,7 @@ def dataset_valid?(json:) # TODO: implement this once we support them in the DB json.present? end - end - end - end - end diff --git a/app/services/application_service.rb b/app/services/application_service.rb index 3a563f1310..2320ec30ed 100644 --- a/app/services/application_service.rb +++ b/app/services/application_service.rb @@ -1,16 +1,13 @@ # frozen_string_literal: true +# Generic methods used throughout the site class ApplicationService - class << self - # Returns either the name specified in dmproadmap.rb initializer or # the Rails application name def application_name - default = Rails.application.class.name.split("::").first + default = Rails.application.class.name.split('::').first _(Rails.configuration.x.application.fetch(:name, default)) end - end - end diff --git a/app/services/external_apis/base_service.rb b/app/services/external_apis/base_service.rb index bd13298aa8..8d8f9c7e9b 100644 --- a/app/services/external_apis/base_service.rb +++ b/app/services/external_apis/base_service.rb @@ -1,15 +1,14 @@ # frozen_string_literal: true -require "httparty" +require 'httparty' module ExternalApis - + # Errors for External Api services class ExternalApiError < StandardError; end + # Abstract service that provides HTTP methods for individual external api services class BaseService - class << self - # The following should be defined in each inheriting service's initializer. # For example: # ExternalApis::RorService.setup do |config| @@ -46,11 +45,11 @@ def active? # `http_get` def headers hash = { - "Content-Type": "application/json", - "Accept": "application/json", - "User-Agent": "#{app_name} (#{app_email})" + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': "#{app_name} (#{app_email})" } - hash.merge({ "Host": URI(api_base_url).hostname.to_s }) + hash.merge({ Host: URI(api_base_url).hostname.to_s }) rescue URI::InvalidURIError => e handle_uri_failure(method: "BaseService.headers #{e.message}", uri: api_base_url) @@ -87,7 +86,7 @@ def app_name # Retrieves the helpdesk email from dmproadmap.rb initializer or uses the contact page url def app_email - dflt = Rails.application.routes.url_helpers.contact_us_url || "" + dflt = Rails.application.routes.url_helpers.contact_us_url || '' Rails.configuration.x.organisation.fetch(:helpdesk_email, dflt) end @@ -117,9 +116,6 @@ def options(additional_headers: {}, debug: false) hash[:debug_output] = $stdout if debug hash end - end - end - end diff --git a/app/services/external_apis/doi_service.rb b/app/services/external_apis/doi_service.rb index ac57dcd56b..ab364a052c 100644 --- a/app/services/external_apis/doi_service.rb +++ b/app/services/external_apis/doi_service.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module ExternalApis - # This service provides an interface to minting/registering DOIs # To enable the feature you will need to: # - Identify a DOI minting authority (e.g. Datacite, Crossref, etc.) @@ -9,9 +8,7 @@ module ExternalApis # - Update the `config/initializers/external_apis/doi.rb` # - Update this service to mint DOIs (based on their API documentation) class DoiService < BaseService - class << self - # Retrieve the config settings from the initializer def landing_page_url Rails.configuration.x.doi&.landing_page_url || super @@ -69,9 +66,6 @@ def mint(plan:) # exports and will become the `dmp_id` in this system's API responses end # rubocop:enable Lint/UnusedMethodArgument - end - end - end diff --git a/app/services/external_apis/open_aire_service.rb b/app/services/external_apis/open_aire_service.rb index 189f849bd0..573dadb591 100644 --- a/app/services/external_apis/open_aire_service.rb +++ b/app/services/external_apis/open_aire_service.rb @@ -1,14 +1,11 @@ # frozen_string_literal: true -require "nokogiri" +require 'nokogiri' module ExternalApis - # This service provides an interface to the OpenAire API. class OpenAireService < BaseService - class << self - # Retrieve the config settings from the initializer def api_base_url Rails.configuration.x.open_aire&.api_base_url || super @@ -34,13 +31,13 @@ def search(funder: default_funder) target = "#{api_base_url}#{search_path % funder}" hdrs = { - "Accept": "application/xml", - "Content-Type": "*/*" + Accept: 'application/xml', + 'Content-Type': '*/*' } resp = http_get(uri: target, additional_headers: hdrs, debug: false) unless resp.code == 200 - handle_http_failure(method: "OpenAire search", http_response: resp) + handle_http_failure(method: 'OpenAire search', http_response: resp) return [] end parse_xml(xml: resp.body) @@ -52,20 +49,17 @@ def search(funder: default_funder) def parse_xml(xml:) return [] unless xml.present? - Nokogiri::XML(xml).xpath("//pair/displayed-value").map do |node| - parts = node.content.split("-") + Nokogiri::XML(xml).xpath('//pair/displayed-value').map do |node| + parts = node.content.split('-') grant_id = parts.shift.to_s.strip - description = parts.join(" - ").strip + description = parts.join(' - ').strip ResearchProject.new(grant_id, description) end # If a JSON parse error occurs then return results of a local table search rescue Nokogiri::XML::SyntaxError => e - log_error(method: "OpenAire search", error: e) + log_error(method: 'OpenAire search', error: e) [] end - end - end - end diff --git a/app/services/external_apis/rdamsc_service.rb b/app/services/external_apis/rdamsc_service.rb new file mode 100644 index 0000000000..b6299a1a2a --- /dev/null +++ b/app/services/external_apis/rdamsc_service.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +module ExternalApis + # This service provides an interface to the RDA Metadata Standards Catalog (RDAMSC) + # It extracts the list of Metadata Standards using two API endpoints from the first extracts + # the list of subjects/concepts from the thesaurus and the second collects the standards + # (aka schemes) and connects them to their appropriate subjects + # + # UI to see the standards: https://rdamsc.bath.ac.uk/scheme-index + # API: + # https://app.swaggerhub.com/apis-docs/alex-ball/rda-metadata-standards-catalog/2.0.0#/m/get_api2_m + class RdamscService < BaseService + class << self + # Retrieve the config settings from the initializer + def landing_page_url + Rails.configuration.x.rdamsc&.landing_page_url || super + end + + def api_base_url + Rails.configuration.x.rdamsc&.api_base_url || super + end + + def max_pages + Rails.configuration.x.rdamsc&.max_pages || super + end + + def max_results_per_page + Rails.configuration.x.rdamsc&.max_results_per_page || super + end + + def max_redirects + Rails.configuration.x.rdamsc&.max_redirects || super + end + + def active? + Rails.configuration.x.rdamsc&.active || super + end + + def schemes_path + Rails.configuration.x.rdamsc&.schemes_path + end + + def thesaurus_path + Rails.configuration.x.rdamsc&.thesaurus_path + end + + def thesaurai + Rails.configuration.x.rdamsc&.thesaurai + end + + def fetch_metadata_standards + query_schemes(path: "#{schemes_path}?pageSize=250") + end + + private + + # Retrieves the full list of metadata schemes from the rdamsc API as JSON. + # For example: + # { + # "apiVersion": "2.0.0", + # "data": { + # "currentItemCount": 10, + # "items": [ + # { + # "description": "

          The Access to Biological Collections Data (ABCD) Schema

          ", + # "keywords": [ + # "http://vocabularies.unesco.org/thesaurus/concept4011", + # "http://vocabularies.unesco.org/thesaurus/concept230", + # "http://rdamsc.bath.ac.uk/thesaurus/subdomain235", + # "http://vocabularies.unesco.org/thesaurus/concept223", + # "http://vocabularies.unesco.org/thesaurus/concept159", + # "http://vocabularies.unesco.org/thesaurus/concept162", + # "http://vocabularies.unesco.org/thesaurus/concept235" + # ], + # "locations": [ + # { "type": "document", "url": "http://www.tdwg.org/standards/115/" }, + # { "type": "website", "url": "http://wiki.tdwg.org/ABCD" } + # ], + # "mscid": "msc:m1", + # "relatedEntities": [ + # { "id": "msc:m42", "role": "child scheme" }, + # { "id": "msc:m43", "role": "child scheme" }, + # { "id": "msc:m64", "role": "child scheme" }, + # { "id": "msc:c1", "role": "input to mapping" }, + # { "id": "msc:c3", "role": "output from mapping" }, + # { "id": "msc:c14", "role": "output from mapping" }, + # { "id": "msc:c18", "role": "output from mapping" }, + # { "id": "msc:c23", "role": "output from mapping" }, + # { "id": "msc:g11", "role": "user" }, + # { "id": "msc:g44", "role": "user" }, + # { "id": "msc:g45", "role": "user" } + # ], + # "slug": "abcd-access-biological-collection-data", + # "title": "ABCD (Access to Biological Collection Data)", + # "uri": "https://rdamsc.bath.ac.uk/api2/m1" + # } + # ] + # } + # } + def query_schemes(path:) + json = query_api(path: path) + return false unless json.present? + + process_scheme_entries(json: json) + return true unless json.fetch('data', {})['nextLink'].present? + + query_schemes(path: json['data']['nextLink']) + end + + def query_api(path:) + return nil unless path.present? + + # Call the API and log any errors + resp = http_get(uri: "#{api_base_url}#{path}", additional_headers: {}, debug: false) + unless resp.present? && resp.code == 200 + handle_http_failure(method: "RDAMSC API query - path: '#{path}' -- ", http_response: resp) + return nil + end + + JSON.parse(resp.body) + rescue JSON::ParserError => e + log_error(method: "RDAMSC API query - path: '#{path}' -- ", error: e) + nil + end + + # rubocop:disable Metrics/AbcSize + def process_scheme_entries(json:) + return false unless json.is_a?(Hash) + + json = json.with_indifferent_access + return false unless json['data'].present? && json['data'].fetch('items', []).any? + + json['data']['items'].each do |item| + standard = MetadataStandard.find_or_create_by(uri: item['uri'], title: item['title']) + standard.update(description: item['description'], locations: item['locations'], + related_entities: item['relatedEntities'], rdamsc_id: item['mscid']) + end + end + # rubocop:enable Metrics/AbcSize + end + end +end diff --git a/app/services/external_apis/re3data_service.rb b/app/services/external_apis/re3data_service.rb new file mode 100644 index 0000000000..35337e4e70 --- /dev/null +++ b/app/services/external_apis/re3data_service.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +module ExternalApis + # This service provides an interface to the Registry of Research Data + # Repositories (re3data.org) API. + # For more information: https://www.re3data.org/api/doc + class Re3dataService < BaseService + class << self + # Retrieve the config settings from the initializer + def landing_page_url + Rails.configuration.x.re3data&.landing_page_url || super + end + + def api_base_url + Rails.configuration.x.re3data&.api_base_url || super + end + + def max_pages + Rails.configuration.x.re3data&.max_pages || super + end + + def max_results_per_page + Rails.configuration.x.re3data&.max_results_per_page || super + end + + def max_redirects + Rails.configuration.x.re3data&.max_redirects || super + end + + def active? + Rails.configuration.x.re3data&.active || super + end + + def list_path + Rails.configuration.x.re3data&.list_path + end + + def repository_path + Rails.configuration.x.re3data&.repository_path + end + + # Retrieves the full list of repositories from the re3data API as XML. + # For example: + # + # + # r3d100000001 + # Odum Institute Archive Dataverse + # + # + # + def fetch + xml_list = query_re3data + return [] unless xml_list.present? + + xml_list.xpath('/list/repository/id').each do |node| + next unless node.present? && node.text.present? + + xml = query_re3data_repository(repo_id: node.text) + next unless xml.present? + + process_repository(id: node.text, node: xml.xpath('//r3d:re3data//r3d:repository').first) + end + end + + private + + # Queries the re3data API for the full list of repositories + def query_re3data + # Call the ROR API and log any errors + resp = http_get(uri: "#{api_base_url}#{list_path}", additional_headers: {}, + debug: false) + + unless resp.present? && resp.code == 200 + handle_http_failure(method: 're3data list', http_response: resp) + return nil + end + Nokogiri.XML(resp.body, nil, 'utf8') + end + + # Queries the re3data API for the specified repository + def query_re3data_repository(repo_id:) + return [] unless repo_id.present? + + target = "#{api_base_url}#{repository_path}#{repo_id}" + # Call the ROR API and log any errors + resp = http_get(uri: target, additional_headers: {}, + debug: false) + + unless resp.present? && resp.code == 200 + handle_http_failure(method: "re3data repository #{repo_id}", http_response: resp) + return [] + end + Nokogiri.XML(resp.body, nil, 'utf8') + end + + # Updates or Creates a repository based on the XML input + def process_repository(id:, node:) + return nil unless id.present? && node.present? + + # Try to find the Repo by the re3data identifier + repo = Repository.find_by(uri: id) + homepage = node.xpath('//r3d:repositoryURL')&.text + name = node.xpath('//r3d:repositoryName')&.text + repo = Repository.find_by(homepage: homepage) unless repo.present? + repo = Repository.find_or_initialize_by(uri: id, name: name) unless repo.present? + repo = parse_repository(repo: repo, node: node) + repo.reload + end + + # Updates the Repository based on the XML input + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def parse_repository(repo:, node:) + return nil unless repo.present? && node.present? + + repo.update( + description: node.xpath('//r3d:description')&.text, + homepage: node.xpath('//r3d:repositoryURL')&.text, + contact: node.xpath('//r3d:repositoryContact')&.text, + info: { + types: node.xpath('//r3d:type').map(&:text), + subjects: node.xpath('//r3d:subject').map(&:text), + provider_types: node.xpath('//r3d:providerType').map(&:text), + keywords: node.xpath('//r3d:keyword').map(&:text), + access: node.xpath('//r3d:databaseAccess//r3d:databaseAccessType')&.text, + pid_system: node.xpath('//r3d:pidSystem')&.text, + policies: node.xpath('//r3d:policy').map { |n| parse_policy(node: n) }, + upload_types: node.xpath('//r3d:dataUpload').map { |n| parse_upload(node: n) } + } + ) + repo + end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + + def parse_policy(node:) + return nil unless node.present? + + { + name: node.xpath('r3d:policyName')&.text, + url: node.xpath('r3d:policyURL')&.text + } + end + + def parse_upload(node:) + return nil unless node.present? + + { + type: node.xpath('r3d:dataUploadType')&.text, + restriction: node.xpath('r3d:dataUploadRestriction')&.text + } + end + end + end +end diff --git a/app/services/external_apis/ror_service.rb b/app/services/external_apis/ror_service.rb index a0ae5f8c38..e639bf2b94 100644 --- a/app/services/external_apis/ror_service.rb +++ b/app/services/external_apis/ror_service.rb @@ -1,14 +1,11 @@ # frozen_string_literal: true module ExternalApis - # This service provides an interface to the Research Organization Registry (ROR) # API. # For more information: https://github.com/ror-community/ror-api class RorService < BaseService - class << self - # Retrieve the config settings from the initializer def landing_page_url Rails.configuration.x.ror&.landing_page_url || super @@ -74,7 +71,7 @@ def search(term:, filters: []) # If a JSON parse error occurs then return results of a local table search rescue JSON::ParserError => e - log_error(method: "ROR search", error: e) + log_error(method: 'ROR search', error: e) [] end @@ -93,7 +90,7 @@ def query_ror(term:, page: 1, filters: []) debug: false) unless resp.present? && resp.code == 200 - handle_http_failure(method: "ROR search", http_response: resp) + handle_http_failure(method: 'ROR search', http_response: resp) return [] end JSON.parse(resp.body) @@ -104,22 +101,23 @@ def query_ror(term:, page: 1, filters: []) def query_string(term:, page: 1, filters: []) query_string = ["query=#{term}", "page=#{page}"] query_string << "filter=#{filters.join(',')}" if filters.any? - query_string.join("&") + query_string.join('&') end # Recursive method that can handle multiple ROR result pages if necessary + # rubocop:disable Metrics/AbcSize def process_pages(term:, json:, filters: []) return [] if json.blank? results = parse_results(json: json) - num_of_results = json.fetch("number_of_results", 1).to_i + num_of_results = json.fetch('number_of_results', 1).to_i # Determine if there are multiple pages of results pages = (num_of_results / max_results_per_page.to_f).to_f.ceil return results unless pages > 1 # Gather the results from the additional page (only up to the max) - (2..(pages > max_pages ? max_pages : pages)).each do |page| + (2..([pages, max_pages].min)).each do |page| json = query_ror(term: term, page: page, filters: filters) results += parse_results(json: json) end @@ -128,42 +126,45 @@ def process_pages(term:, json:, filters: []) # If we encounter a JSON parse error on subsequent page requests then just # return what we have so far rescue JSON::ParserError => e - log_error(method: "ROR search", error: e) + log_error(method: 'ROR search', error: e) results || [] end + # rubocop:enable Metrics/AbcSize # Convert the JSON items into a hash + # rubocop:disable Metrics/AbcSize def parse_results(json:) results = [] - return results unless json.present? && json.fetch("items", []).any? + return results unless json.present? && json.fetch('items', []).any? - json["items"].each do |item| - next unless item["id"].present? && item["name"].present? + json['items'].each do |item| + next unless item['id'].present? && item['name'].present? results << { - ror: item["id"].gsub(/^#{landing_page_url}/, ""), + ror: item['id'].gsub(/^#{landing_page_url}/, ''), name: org_name(item: item), - sort_name: item["name"], - url: item.fetch("links", []).first, + sort_name: item['name'], + url: item.fetch('links', []).first, language: org_language(item: item), fundref: fundref_id(item: item), - abbreviation: item.fetch("acronyms", []).first + abbreviation: item.fetch('acronyms', []).first } end results end + # rubocop:enable Metrics/AbcSize # 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? + return '' unless item.present? && item['name'].present? - country = item.fetch("country", {}).fetch("country_name", "") + country = item.fetch('country', {}).fetch('country_name', '') website = org_website(item: item) # If no website or country then just return the name - return item["name"] unless website.present? || country.present? + return item['name'] unless website.present? || country.present? # Otherwise return the contextualized name "#{item['name']} (#{website || country})" @@ -171,39 +172,36 @@ def org_name(item:) # Extracts the org's ISO639 if available def org_language(item:) - dflt = I18n.default_locale || "en" + dflt = I18n.default_locale || 'en' return dflt unless item.present? - labels = item.fetch("labels", [{ "iso639": dflt }]) - labels.first&.fetch("iso639", I18n.default_locale) || dflt + labels = item.fetch('labels', [{ iso639: dflt }]) + labels.first&.fetch('iso639', I18n.default_locale) || dflt end # Extracts the website domain from the item def org_website(item:) - return nil unless item.present? && item.fetch("links", [])&.any? - return nil if item["links"].first.blank? + return nil unless item.present? && item.fetch('links', [])&.any? + return nil if item['links'].first.blank? # 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 - website.gsub("www.", "") + website = item['links'].first.scan(domain_regex).last.first + website.gsub('www.', '') end # Extracts the FundRef Id if available def fundref_id(item:) - return "" unless item.present? && item["external_ids"].present? - return "" unless item["external_ids"].fetch("FundRef", {}).any? + return '' unless item.present? && item['external_ids'].present? + return '' unless item['external_ids'].fetch('FundRef', {}).any? # If a preferred Id was specified then use it - ret = item["external_ids"].fetch("FundRef", {}).fetch("preferred", "") + ret = item['external_ids'].fetch('FundRef', {}).fetch('preferred', '') return ret if ret.present? # Otherwise take the first one listed - item["external_ids"].fetch("FundRef", {}).fetch("all", []).first + item['external_ids'].fetch('FundRef', {}).fetch('all', []).first end - end - end - end diff --git a/app/services/external_apis/spdx_service.rb b/app/services/external_apis/spdx_service.rb new file mode 100644 index 0000000000..054e96d2a9 --- /dev/null +++ b/app/services/external_apis/spdx_service.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module ExternalApis + # This service provides an interface to the SPDX License List + # For more information: https://spdx.org/licenses/index.html + class SpdxService < BaseService + class << self + # Retrieve the config settings from the initializer + def landing_page_url + Rails.configuration.x.spdx&.landing_page_url || super + end + + def api_base_url + Rails.configuration.x.spdx&.api_base_url || super + end + + def max_pages + Rails.configuration.x.spdx&.max_pages || super + end + + def max_results_per_page + Rails.configuration.x.spdx&.max_results_per_page || super + end + + def max_redirects + Rails.configuration.x.spdx&.max_redirects || super + end + + def active? + Rails.configuration.x.spdx&.active || super + end + + def list_path + Rails.configuration.x.spdx&.list_path + end + + # Retrieves the full list of license from the SPDX Github repository. + # For example: + # "licenses": [ + # { + # "reference": "./0BSD.html", + # "isDeprecatedLicenseId": false, + # "detailsUrl": "http://spdx.org/licenses/0BSD.json", + # "referenceNumber": "67", + # "name": "BSD Zero Clause License", + # "licenseId": "0BSD", + # "seeAlso": [ + # "http://landley.net/toybox/license.html" + # ], + # "isOsiApproved": true + # } + # ] + def fetch + licenses = query_spdx + return [] unless licenses.present? + + licenses.each { |license| process_license(hash: license) } + License.all + end + + private + + # Queries the re3data API for the full list of repositories + def query_spdx + # Call the ROR API and log any errors + resp = http_get(uri: "#{api_base_url}#{list_path}", additional_headers: {}, debug: false) + + unless resp.present? && resp.code == 200 + handle_http_failure(method: 'SPDX list', http_response: resp) + return [] + end + json = JSON.parse(resp.body) + return [] unless json.fetch('licenses', []).any? + + json['licenses'] + rescue JSON::ParserError => e + log_error(method: 'SPDX search', error: e) + [] + end + + # Updates or Creates a repository based on the XML input + def process_license(hash:) + return nil unless hash.present? + + hash = hash.with_indifferent_access + license = License.find_or_initialize_by(identifier: hash['licenseId']) + return nil unless license.present? + + license.update( + name: hash['name'], + uri: hash['detailsUrl'], + osi_approved: hash['isOsiApproved'], + deprecated: hash['isDeprecatedLicenseId'] + ) + end + end + end +end diff --git a/app/services/locale_service.rb b/app/services/locale_service.rb index 2c2c87e946..b091221c13 100644 --- a/app/services/locale_service.rb +++ b/app/services/locale_service.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true +# Helpers methods for handling I18n and GetText class LocaleService - class << self - # Returns the default locale/language def default_locale abbrev = Language.default.try(:abbreviation) if Language.table_exists? @@ -14,9 +13,7 @@ def default_locale # Returns the available locales/languages def available_locales - # rubocop:disable Layout/LineLength locales = Language.sorted_by_abbreviation.pluck(:abbreviation).presence if Language.table_exists? - # rubocop:enable Layout/LineLength locales.present? ? locales : [default_locale] end @@ -44,7 +41,5 @@ def convert(string:, join_char: Rails.configuration.x.locales.gettext_join_chara region.upcase! if region.present? region.present? ? "#{language}#{join_char}#{region}" : language end - end - end diff --git a/app/services/org/create_created_plan_service.rb b/app/services/org/create_created_plan_service.rb index 8e3297bb8f..2f88950305 100644 --- a/app/services/org/create_created_plan_service.rb +++ b/app/services/org/create_created_plan_service.rb @@ -11,12 +11,11 @@ Perm.class Template.class +# Org usage --- TODO: This should likely be a module class Org - + # Usage for Nbr of created plans class CreateCreatedPlanService - class << self - def call(org = nil, threads: 0) orgs = org.nil? ? Org.all : [org] @@ -37,9 +36,6 @@ def call(org = nil, threads: 0) end end end - end - end - end diff --git a/app/services/org/create_exported_plan_service.rb b/app/services/org/create_exported_plan_service.rb index 4bc3db22d6..7fe475b98b 100644 --- a/app/services/org/create_exported_plan_service.rb +++ b/app/services/org/create_exported_plan_service.rb @@ -10,12 +10,11 @@ User.class ExportedPlan.class +# Org usage --- TODO: This should likely be a module class Org - + # Usage for Nbr of exported plans class CreateExportedPlanService - class << self - def call(org = nil, threads: 0) orgs = org.nil? ? Org.all : [org] @@ -35,9 +34,6 @@ def call(org = nil, threads: 0) end end end - end - end - end diff --git a/app/services/org/create_joined_user_service.rb b/app/services/org/create_joined_user_service.rb index 441bf937a5..cc8707467f 100644 --- a/app/services/org/create_joined_user_service.rb +++ b/app/services/org/create_joined_user_service.rb @@ -7,12 +7,11 @@ StatJoinedUser::CreateOrUpdate.class User.class +# Org usage --- TODO: This should likely be a module class Org - + # Usage for Nbr of created users class CreateJoinedUserService - class << self - def call(org = nil, threads: 0) orgs = org.nil? ? Org.all : [org] @@ -27,9 +26,6 @@ def call(org = nil, threads: 0) end # pp StatJoinedUser.where.not(count: 0) end - end - end - end diff --git a/app/services/org/create_last_month_created_plan_service.rb b/app/services/org/create_last_month_created_plan_service.rb index 22abd20029..6176b4bd30 100644 --- a/app/services/org/create_last_month_created_plan_service.rb +++ b/app/services/org/create_last_month_created_plan_service.rb @@ -11,12 +11,11 @@ Perm.class Template.class +# Org usage --- TODO: This should likely be a module class Org - + # Usage for Nbr of created plans in the prior month class CreateLastMonthCreatedPlanService - class << self - def call(org = nil, threads: 0) orgs = org.nil? ? Org.all : [org] @@ -38,9 +37,6 @@ def call(org = nil, threads: 0) ) end end - end - end - end diff --git a/app/services/org/create_last_month_exported_plan_service.rb b/app/services/org/create_last_month_exported_plan_service.rb index 37d5a4619d..39b474df10 100644 --- a/app/services/org/create_last_month_exported_plan_service.rb +++ b/app/services/org/create_last_month_exported_plan_service.rb @@ -10,12 +10,11 @@ User.class ExportedPlan.class +# Org usage --- TODO: This should likely be a module class Org - + # Usage for Nbr of exported plans in the prior month class CreateLastMonthExportedPlanService - class << self - def call(org = nil, threads: 0) orgs = org.nil? ? Org.all : [org] @@ -37,9 +36,6 @@ def call(org = nil, threads: 0) ) end end - end - end - end diff --git a/app/services/org/create_last_month_joined_user_service.rb b/app/services/org/create_last_month_joined_user_service.rb index b2d66f3d9c..db45dd6f6d 100644 --- a/app/services/org/create_last_month_joined_user_service.rb +++ b/app/services/org/create_last_month_joined_user_service.rb @@ -7,12 +7,11 @@ StatJoinedUser::CreateOrUpdate.class User.class +# Org usage --- TODO: This should likely be a module class Org - + # Usage for Nbr of created users in the prior month class CreateLastMonthJoinedUserService - class << self - def call(org = nil, threads: 0) orgs = org.nil? ? ::Org.all : [org] @@ -28,9 +27,6 @@ def call(org = nil, threads: 0) ) end end - end - end - end diff --git a/app/services/org/create_last_month_shared_plan_service.rb b/app/services/org/create_last_month_shared_plan_service.rb index c7bca818be..ca6305ea2b 100644 --- a/app/services/org/create_last_month_shared_plan_service.rb +++ b/app/services/org/create_last_month_shared_plan_service.rb @@ -9,12 +9,11 @@ Plan.class Role.class +# Org usage --- TODO: This should likely be a module class Org - + # Usage for Nbr of shared plans in the prior month class CreateLastMonthSharedPlanService - class << self - def call(org = nil, threads: 0) orgs = org.nil? ? Org.all : [org] @@ -36,9 +35,6 @@ def call(org = nil, threads: 0) ) end end - end - end - end diff --git a/app/services/org/create_shared_plan_service.rb b/app/services/org/create_shared_plan_service.rb index ec75ba4f42..7a914816ee 100644 --- a/app/services/org/create_shared_plan_service.rb +++ b/app/services/org/create_shared_plan_service.rb @@ -9,12 +9,11 @@ Plan.class Role.class +# Org usage --- TODO: This should likely be a module class Org - + # Usage for Nbr of shared plans class CreateSharedPlanService - class << self - def call(org = nil, threads: 0) orgs = org.nil? ? Org.all : [org] @@ -34,9 +33,6 @@ def call(org = nil, threads: 0) end end end - end - end - end diff --git a/app/services/org/monthly_usage_service.rb b/app/services/org/monthly_usage_service.rb index c6984d7f04..6d6b460181 100644 --- a/app/services/org/monthly_usage_service.rb +++ b/app/services/org/monthly_usage_service.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true +# Org usage --- TODO: This should likely be a module class Org - + # Usage by month class MonthlyUsageService - class << self - def call(current_user, filtered: false) total = build_from_joined_user(current_user, filtered) build_from_created_plan(current_user, filtered, total) @@ -27,7 +26,7 @@ def build_model(month:, new_plans: 0, new_users: 0, downloads: 0, plans_shared: end def reducer_body(acc, rec, key_target) - month = rec.date.strftime("%b-%y") + month = rec.date.strftime('%b-%y') count = rec.count if acc[month].present? @@ -72,9 +71,6 @@ def build_from_exported_plan(current_user, filtered, total = {}) reducer_body(acc, rec, :downloads) end end - end - end - end diff --git a/app/services/org/total_count_created_plan_service.rb b/app/services/org/total_count_created_plan_service.rb index 5739792b3c..3c4a5af4c4 100644 --- a/app/services/org/total_count_created_plan_service.rb +++ b/app/services/org/total_count_created_plan_service.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true +# Org usage --- TODO: This should likely be a module class Org - + # Usage - total nbr of created plans class TotalCountCreatedPlanService - class << self - def call(org = nil, filtered: false) return for_orgs(filtered) unless org.present? @@ -18,8 +17,8 @@ def for_orgs(filtered) result = ::StatCreatedPlan .where(filtered: filtered) .includes(:org) - .select(:"orgs.name", :count) - .group(:"orgs.name") + .select(:'orgs.name', :count) + .group(:'orgs.name') .sum(:count) result.each_pair.map do |pair| build_model(org_name: pair[0], count: pair[1].to_i) @@ -34,9 +33,6 @@ def for_org(org, filtered) def build_model(org_name:, count:) { org_name: org_name, count: count } end - end - end - end diff --git a/app/services/org/total_count_joined_user_service.rb b/app/services/org/total_count_joined_user_service.rb index a07147c82f..13695c412c 100644 --- a/app/services/org/total_count_joined_user_service.rb +++ b/app/services/org/total_count_joined_user_service.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true +# Org usage --- TODO: This should likely be a module class Org - + # Usage - total nbr of created users class TotalCountJoinedUserService - class << self - def call(org = nil, filtered: false) return for_orgs(filtered) unless org.present? @@ -18,8 +17,8 @@ def for_orgs(filtered) result = ::StatJoinedUser .where(filtered: filtered) .includes(:org) - .select(:"orgs.name", :count) - .group(:"orgs.name") + .select(:'orgs.name', :count) + .group(:'orgs.name') .sum(:count) result.each_pair.map do |pair| build_model(org_name: pair[0], count: pair[1].to_i) @@ -34,9 +33,6 @@ def for_org(org, filtered) def build_model(org_name:, count:) { org_name: org_name, count: count } end - end - end - end diff --git a/app/services/org/total_count_stat_service.rb b/app/services/org/total_count_stat_service.rb index 78a15507f7..a8c1062b98 100644 --- a/app/services/org/total_count_stat_service.rb +++ b/app/services/org/total_count_stat_service.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true +# Org usage --- TODO: This should likely be a module class Org - + # Usage - totals class TotalCountStatService - class << self - def call(filtered: false) total = build_from_joined_user build_from_created_plan(filtered, total) @@ -51,9 +50,6 @@ def build_from_created_plan(filtered, total = {}) reducer_body(acc, count, :total_plans) end end - 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 7f54dbc25a..27b2e6ef5a 100644 --- a/app/services/org_selection/hash_to_org_service.rb +++ b/app/services/org_selection/hash_to_org_service.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require "text" +require 'text' module OrgSelection - # This class provides conversion methods for turning OrgSelection::Search # results into Orgs and Identifiers # For example: @@ -17,9 +16,7 @@ module OrgSelection # identifier (ROR) = "http://example.org/123" # class HashToOrgService - class << self - def to_org(hash:, allow_create: true) return nil unless hash.present? @@ -43,6 +40,7 @@ def to_org(hash:, allow_create: true) allow_create ? initialize_org(hash: hash) : nil end + # rubocop:disable Metrics/AbcSize def to_identifiers(hash:) return [] unless hash.present? @@ -61,6 +59,7 @@ def to_identifiers(hash:) end out end + # rubocop:enable Metrics/AbcSize private @@ -81,9 +80,10 @@ def lookup_org_by_identifiers(hash:) # Lookup the Org by its :name def lookup_org_by_name(hash:) clean_name = OrgSelection::SearchService.name_without_alias(name: hash[:name]) - ## org = Org.search(clean_name).first - ## Part of ISSUE149: switch to exact match to solve the bug that 'test' cannot be saved if 'any_org_with_test_as_substring' exist already - ## if duplicate org name, return the first (i.e. this org exists) + # org = Org.search(clean_name).first + # Part of ISSUE149: if 'any_org_with_test_as_substring' exist already + # then switch to exact match to solve the bug that 'test' cannot be saved + # if duplicate org name, return the first (i.e. this org exists) org = Org.where(name: clean_name).first exact_match?(rec: org, name2: hash[:name]) ? org : nil end @@ -92,7 +92,7 @@ def lookup_org_by_name(hash:) def initialize_org(hash:) return nil unless hash.present? && hash[:name].present? - org = Org.new( + Org.new( name: hash[:name], links: links_from_hash(name: hash[:name], website: hash[:url]), language: language_from_hash(hash: hash), @@ -101,14 +101,13 @@ def initialize_org(hash:) is_other: false, abbreviation: abbreviation_from_hash(hash: hash) ) - org end # Convert the name and website into Org.links def links_from_hash(name:, website:) - return { "org": [] } unless name.present? && website.present? + return { org: [] } unless name.present? && website.present? - { "org": [{ "link": website, "text": name }] } + { org: [{ link: website, text: name }] } end # Converts the Org name over to a unique abbreviation @@ -119,7 +118,7 @@ def abbreviation_from_hash(hash:) # Get the first letter of each word if no abbreviiation was provided OrgSelection::SearchService.name_without_alias(name: hash[:name]) - .split(" ").map(&:first).join.upcase + .split.map(&:first).join.upcase end # Get the language from the hash or use the default @@ -145,9 +144,6 @@ def exact_match?(rec:, name2:) OrgSelection::SearchService.exact_match?(name1: rec.name, name2: name2) end - end - end - end diff --git a/app/services/org_selection/org_to_hash_service.rb b/app/services/org_selection/org_to_hash_service.rb index 0f37ee18c9..01085ec5aa 100644 --- a/app/services/org_selection/org_to_hash_service.rb +++ b/app/services/org_selection/org_to_hash_service.rb @@ -1,15 +1,12 @@ # frozen_string_literal: true -require "text" +require 'text' module OrgSelection - # This class provides a search mechanism for Orgs that looks at records in the # the database along with any available external APIs class OrgToHashService - class << self - # Convert an Identifiable Model over to hash results like: # An Org with id = 123, name = "Foo (foo.org)", # identifier (ROR) = "http://example.org/123" @@ -36,9 +33,6 @@ def to_hash(org:) end out end - end - end - end diff --git a/app/services/org_selection/search_service.rb b/app/services/org_selection/search_service.rb index 891aa9537c..67aa023c5d 100644 --- a/app/services/org_selection/search_service.rb +++ b/app/services/org_selection/search_service.rb @@ -1,15 +1,12 @@ # frozen_string_literal: true -require "text" +require 'text' module OrgSelection - # This class provides a search mechanism for Orgs that looks at records in the # the database along with any available external APIs class SearchService - class << self - # Search for organizations both locally and externally def search_combined(search_term:) return [] unless search_term.present? && search_term.length > 2 @@ -56,9 +53,9 @@ def exact_match?(name1:, name2:) # Removes the parenthesis portion of the name. For example: # "Foo College (foo.edu)" --> "Foo College" def name_without_alias(name:) - return "" unless name.present? + return '' unless name.present? - name.split(" (")&.first&.strip + name.split(' (')&.first&.strip end private @@ -71,7 +68,7 @@ def expiry def local_search(search_term:) return [] unless search_term.present? - Rails.cache.fetch(["org_selection-local", search_term], expires_in: expiry) do + Rails.cache.fetch(['org_selection-local', search_term], expires_in: expiry) do Org.includes(identifiers: :identifier_scheme) .search(name_without_alias(name: search_term)).to_a end @@ -80,7 +77,7 @@ def local_search(search_term:) def externals_search(search_term:) return [] unless ExternalApis::RorService.active? && search_term.present? - Rails.cache.fetch(["org_selection-ror", search_term], expires_in: expiry) do + Rails.cache.fetch(['org_selection-ror', search_term], expires_in: expiry) do ExternalApis::RorService.search(term: search_term) end end @@ -172,9 +169,6 @@ def filter(array:) hash.fetch(:score, 0) <= 25 || hash.fetch(:weight, 1) < 2 end end - end - end - end diff --git a/app/services/template/upgrade_customization_service.rb b/app/services/template/upgrade_customization_service.rb index 0a859150a9..5a055f8199 100644 --- a/app/services/template/upgrade_customization_service.rb +++ b/app/services/template/upgrade_customization_service.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Template - # Service object to upgrade a customization Template with new changes from the original # funder Template. Remember: {updated_template} is a customization of funder Template. # @@ -22,7 +21,6 @@ class Template # {#updated_template} # class UpgradeCustomizationService - # Exception raised when the Template is not a customization. class NotACustomizationError < StandardError end @@ -61,17 +59,16 @@ def initialize(template) # Run the script # # Returns {Template} + # rubocop:disable Metrics/MethodLength def call Template.transaction do if @source_template.customization_of.blank? raise NotACustomizationError, - _("upgrade_customization! requires a customised template") + _('upgrade_customization! requires a customised template') end if @original_funder_template.nil? - # rubocop:disable Layout/LineLength raise NoFunderTemplateError, - _("upgrade cannot be carried out since there is no published template of its current funder") - # rubocop:enable Layout/LineLength + _('upgrade cannot be carried out since there is no published template of its current funder') end # Merges modifiable sections or questions from source into updated_template object @@ -95,6 +92,7 @@ def call end @updated_template end + # rubocop:enable Metrics/MethodLength private @@ -160,7 +158,5 @@ def sort_sections_within_phase(phase) def template_org @source_template.org end - end - end diff --git a/app/validators/after_validator.rb b/app/validators/after_validator.rb index d5cb6c5e6e..c241cf4a6f 100644 --- a/app/validators/after_validator.rb +++ b/app/validators/after_validator.rb @@ -1,16 +1,17 @@ # frozen_string_literal: true +# Validation to ensure that an end date must come after a begin/start date class AfterValidator < ActiveModel::EachValidator + DEFAULT_MESSAGE = _('must be after %{date}') - DEFAULT_MESSAGE = _("must be after %{date}") - + # rubocop:disable Metrics/AbcSize def validate_each(record, attribute, value) return if value.nil? - return if record.persisted? && options[:on].to_s == "create" - return if record.new_record? && options[:on].to_s == "update" + return if record.persisted? && options[:on].to_s == 'create' + return if record.new_record? && options[:on].to_s == 'update' - msg = options.fetch(:message, DEFAULT_MESSAGE % { date: options[:date] }) + msg = options.fetch(:message, format(DEFAULT_MESSAGE, date: options[:date])) record.errors.add(attribute, msg) if value.to_date < options[:date] end - + # rubocop:enable Metrics/AbcSize end diff --git a/app/validators/answer_for_correct_template_validator.rb b/app/validators/answer_for_correct_template_validator.rb index 4aa899354f..20723a8eb5 100644 --- a/app/validators/answer_for_correct_template_validator.rb +++ b/app/validators/answer_for_correct_template_validator.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true +# Validation to ensure that the question for the plan exists in the template class AnswerForCorrectTemplateValidator < ActiveModel::Validator - def validate(record) return if record.plan.nil? || record.question.nil? # Make sure that the question and plan belong to the same template! return unless record.plan.template == record.question.section.phase.template - record.errors[:question] << I18n.t("helpers.answer.question_must_belong_to_correct_template") + record.errors[:question] << I18n.t('helpers.answer.question_must_belong_to_correct_template') end - end diff --git a/app/validators/email_validator.rb b/app/validators/email_validator.rb index b53c79873c..05e46d9efb 100644 --- a/app/validators/email_validator.rb +++ b/app/validators/email_validator.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true +# Validation for email format class EmailValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) return if value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i - record.errors[attribute] << (options[:message] || "is not a valid email address") + record.errors[attribute] << (options[:message] || 'is not a valid email address') end - end diff --git a/app/validators/org_links_validator.rb b/app/validators/org_links_validator.rb index 3d5e3124b2..9515a6bb2a 100644 --- a/app/validators/org_links_validator.rb +++ b/app/validators/org_links_validator.rb @@ -1,17 +1,16 @@ # frozen_string_literal: true +# Validation for the format of the JSON for Org links class OrgLinksValidator < ActiveModel::Validator - - include JSONLinkValidator + include JsonLinkValidator def validate(record) links = record.links if links.is_a?(Hash) - unless links.with_indifferent_access.key?("org") - record.errors[:links] << _('A key "org" is expected for links hash') % { key: k } + unless links.with_indifferent_access.key?('org') + record.errors[:links] << (format(_('A key "org" is expected for links hash'), key: k)) end else - record.errors[:links] << _("A hash is expected for links") + record.errors[:links] << _('A hash is expected for links') end end - end diff --git a/app/validators/template_links_validator.rb b/app/validators/template_links_validator.rb index 4ccf44a370..a4440dca26 100644 --- a/app/validators/template_links_validator.rb +++ b/app/validators/template_links_validator.rb @@ -1,25 +1,27 @@ # frozen_string_literal: true +# Validation for the format of the JSON for Template links class TemplateLinksValidator < ActiveModel::Validator + include JsonLinkValidator - include JSONLinkValidator + # rubocop:disable Metrics/AbcSize def validate(record) links = record.links expected_keys = %w[funder sample_plan] if links.is_a?(Hash) expected_keys.each do |k| - if !links.key?(k) - record.errors[:links] << _("A key %{key} is expected for links hash") % { key: k } - else + if links.key?(k) unless valid_links?(links[k]) - msg = _("The key %{key} does not have a valid set of object links") - record.errors[:links] << msg % { key: k } + msg = _('The key %{key} does not have a valid set of object links') + record.errors[:links] << (format(msg, key: k)) end + else + record.errors[:links] << (format(_('A key %{key} is expected for links hash'), key: k)) end end else - record.errors[:links] << _("A hash is expected for links") + record.errors[:links] << _('A hash is expected for links') end end - + # rubocop:enable Metrics/AbcSize end diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb index 5c40574527..cdaed9416f 100644 --- a/app/validators/url_validator.rb +++ b/app/validators/url_validator.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true +# Validation for URL format class UrlValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) reg = %r{https?://[-a-zA-Z0-9@:%_+.~#?&/=]{2,256}\.[a-z]{2,4}\b(/[-a-zA-Z0-9@:%_+.~#?&/=]*)?} return unless value =~ reg - record.errors[attribute] << (options[:message] || "is not a valid URL") + record.errors[attribute] << (options[:message] || 'is not a valid URL') end - end diff --git a/app/views/answers/_new_edit.html.erb b/app/views/answers/_new_edit.html.erb index bfdb5bb7e8..f3804a2e68 100644 --- a/app/views/answers/_new_edit.html.erb +++ b/app/views/answers/_new_edit.html.erb @@ -73,7 +73,7 @@ <% if annotation.present? && annotation.org.present? && annotation.text.present? %>
          - <%=_('Example')%> + <%="#{annotation.org.abbreviation} "%><%=_('example answer')%>
          <%= sanitize annotation.text %>
          diff --git a/app/views/answers/_status.html.erb b/app/views/answers/_status.html.erb index bf893365a2..fcda55c68a 100644 --- a/app/views/answers/_status.html.erb +++ b/app/views/answers/_status.html.erb @@ -2,8 +2,24 @@ - - +<% + helpdesk_email = Rails.configuration.x.organisation.helpdesk_email + email_subject = _('Plan writing error related to %{tool_name}') %{ :tool_name => tool_name } +%> + + <% if answer.answered? %> <%= _('Answered')%> diff --git a/app/views/api/v0/statistics/plans.json.jbuilder b/app/views/api/v0/statistics/plans.json.jbuilder index bfef9c9061..c76e9ea751 100644 --- a/app/views/api/v0/statistics/plans.json.jbuilder +++ b/app/views/api/v0/statistics/plans.json.jbuilder @@ -2,6 +2,7 @@ json.prettify! +# rubocop:disable Metrics/BlockLength json.plans @org_plans.each do |plan| json.id plan.id json.grant_number plan.grant&.value @@ -14,15 +15,19 @@ json.plans @org_plans.each do |plan| end json.funder do - json.name plan.template.org.funder? ? plan.template.org.name : "" + json.name plan.template.org.funder? ? plan.template.org.name : '' end json.principal_investigator do json.name plan.contributors.investigation.first&.name end + json.owner do + json.email plan.owner.present? ? plan.owner.email : "" + end + json.data_contact do - json.info plan.contributors.data_curation.first&.name + json.info plan.contributors.data_curation.first&.name end json.description plan.description @@ -35,3 +40,4 @@ json.plans @org_plans.each do |plan| json.answered_questions plan.answers.count end end +# rubocop:enable Metrics/BlockLength diff --git a/app/views/api/v1/contributors/_show.json.jbuilder b/app/views/api/v1/contributors/_show.json.jbuilder index 757f5db7c9..833ea6ab72 100644 --- a/app/views/api/v1/contributors/_show.json.jbuilder +++ b/app/views/api/v1/contributors/_show.json.jbuilder @@ -4,36 +4,34 @@ is_contact ||= false -json.name contributor.name +json.name contributor.is_a?(User) ? contributor.name(false) : contributor.name json.mbox contributor.email -unless is_contact - if contributor.selected_roles.any? - roles = contributor.selected_roles.map do |role| - Api::V1::ContributorPresenter.role_as_uri(role: role) - end - json.role roles if roles.any? +if !is_contact && contributor.selected_roles.any? + roles = contributor.selected_roles.map do |role| + Api::V1::ContributorPresenter.role_as_uri(role: role) end + json.role roles if roles.any? end if contributor.org.present? json.affiliation do - json.partial! "api/v1/orgs/show", org: contributor.org + json.partial! 'api/v1/orgs/show', org: contributor.org end end -orcid = contributor.identifier_for_scheme(scheme: "orcid") +orcid = contributor.identifier_for_scheme(scheme: 'orcid') if orcid.present? id = Api::V1::ContributorPresenter.contributor_id( identifiers: contributor.identifiers ) if is_contact json.contact_id do - json.partial! "api/v1/identifiers/show", identifier: id + json.partial! 'api/v1/identifiers/show', identifier: id end else json.contributor_id do - json.partial! "api/v1/identifiers/show", identifier: id + json.partial! 'api/v1/identifiers/show', identifier: id end end end diff --git a/app/views/api/v1/datasets/_show.json.jbuilder b/app/views/api/v1/datasets/_show.json.jbuilder index 3a3bb0b3b3..2aab9e3a7b 100644 --- a/app/views/api/v1/datasets/_show.json.jbuilder +++ b/app/views/api/v1/datasets/_show.json.jbuilder @@ -1,29 +1,83 @@ # frozen_string_literal: true -# locals: plan +# locals: output -presenter = Api::V1::PlanPresenter.new(plan: plan) +if output.is_a?(ResearchOutput) + presenter = Api::V1::ResearchOutputPresenter.new(output: output) -json.title "Generic Dataset" -json.personal_data "unknown" -json.sensitive_data "unknown" + json.type output.output_type + json.title output.title + json.description output.description + json.personal_data Api::V1::ApiPresenter.boolean_to_yes_no_unknown(value: output.personal_data) + json.sensitive_data Api::V1::ApiPresenter.boolean_to_yes_no_unknown(value: output.sensitive_data) + json.issued output.release_date&.to_formatted_s(:iso8601) -json.dataset_id do - json.partial! "api/v1/identifiers/show", identifier: presenter.identifier -end + json.preservation_statement presenter.preservation_statement + json.security_and_privacy presenter.security_and_privacy + json.data_quality_assurance presenter.data_quality_assurance -json.distribution [plan] do |distribution| - json.title "PDF - #{distribution.title}" - json.data_access "open" - json.download_url plan_export_url(distribution, format: :pdf) - json.format do - json.array! ["application/pdf"] + json.dataset_id do + json.partial! "api/v1/identifiers/show", identifier: presenter.dataset_id end -end -if plan.research_domain_id.present? - research_domain = ResearchDomain.find_by(id: plan.research_domain_id) - if research_domain.present? - json.keyword [research_domain.label, "#{research_domain.identifier} - #{research_domain.label}"] + 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.select { |loc| loc["type"] == "website" }.first + 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/v1/error.json.jbuilder b/app/views/api/v1/error.json.jbuilder index 6cf9443c02..20ee5939dd 100644 --- a/app/views/api/v1/error.json.jbuilder +++ b/app/views/api/v1/error.json.jbuilder @@ -1,6 +1,6 @@ # frozen_string_literal: true -json.partial! "api/v1/standard_response" +json.partial! 'api/v1/standard_response' json.items [] json.errors @payload[:errors] diff --git a/app/views/api/v1/heartbeat.json.jbuilder b/app/views/api/v1/heartbeat.json.jbuilder index af0fcdbdbb..ba0b561899 100644 --- a/app/views/api/v1/heartbeat.json.jbuilder +++ b/app/views/api/v1/heartbeat.json.jbuilder @@ -1,5 +1,5 @@ # frozen_string_literal: true -json.partial! "api/v1/standard_response" +json.partial! 'api/v1/standard_response' json.items [] diff --git a/app/views/api/v1/orgs/_show.json.jbuilder b/app/views/api/v1/orgs/_show.json.jbuilder index 69b7ebaa0a..65ba3f39dd 100644 --- a/app/views/api/v1/orgs/_show.json.jbuilder +++ b/app/views/api/v1/orgs/_show.json.jbuilder @@ -9,6 +9,6 @@ json.region org.region&.abbreviation if org.identifiers.any? json.affiliation_id do id = Api::V1::OrgPresenter.affiliation_id(identifiers: org.identifiers) - json.partial! "api/v1/identifiers/show", identifier: id + json.partial! 'api/v1/identifiers/show', identifier: id end end diff --git a/app/views/api/v1/plans/_funding.json.jbuilder b/app/views/api/v1/plans/_funding.json.jbuilder index 725cda1358..72b4204dc7 100644 --- a/app/views/api/v1/plans/_funding.json.jbuilder +++ b/app/views/api/v1/plans/_funding.json.jbuilder @@ -9,14 +9,14 @@ if plan.funder.present? if id.present? json.funder_id do - json.partial! "api/v1/identifiers/show", identifier: id + json.partial! 'api/v1/identifiers/show', identifier: id end end end if plan.grant_id.present? && plan.grant.present? json.grant_id do - json.partial! "api/v1/identifiers/show", identifier: plan.grant + json.partial! 'api/v1/identifiers/show', identifier: plan.grant end end @@ -29,7 +29,7 @@ json.funding_status Api::V1::FundingPresenter.status(plan: plan) # 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/v1/identifiers/show", identifier: Identifier.new(identifiable: plan, + json.partial! 'api/v1/identifiers/show', identifier: Identifier.new(identifiable: plan, value: plan.identifier) end end @@ -37,5 +37,5 @@ 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/v1/orgs/show", org: funded_org + json.partial! 'api/v1/orgs/show', org: funded_org end diff --git a/app/views/api/v1/plans/_project.json.jbuilder b/app/views/api/v1/plans/_project.json.jbuilder index bb4ba3ef0f..df656a2a48 100644 --- a/app/views/api/v1/plans/_project.json.jbuilder +++ b/app/views/api/v1/plans/_project.json.jbuilder @@ -8,11 +8,11 @@ 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 +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/v1/plans/funding", plan: plan + json.partial! 'api/v1/plans/funding', plan: plan end end diff --git a/app/views/api/v1/plans/_show.json.jbuilder b/app/views/api/v1/plans/_show.json.jbuilder index 6f8d226f5f..f5cbc87c35 100644 --- a/app/views/api/v1/plans/_show.json.jbuilder +++ b/app/views/api/v1/plans/_show.json.jbuilder @@ -2,9 +2,13 @@ # locals: plan -json.schema "https://github.com/RDA-DMP-Common/RDA-DMP-Common-Standard/tree/master/examples/JSON/JSON-schema/1.0" +json.schema 'https://github.com/RDA-DMP-Common/RDA-DMP-Common-Standard/tree/master/examples/JSON/JSON-schema/1.0' presenter = Api::V1::PlanPresenter.new(plan: plan) + +# 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 @@ -22,13 +26,13 @@ json.ethical_issues_report plan.ethical_issues_report id = presenter.identifier if id.present? json.dmp_id do - json.partial! "api/v1/identifiers/show", identifier: id + json.partial! 'api/v1/identifiers/show', identifier: id end end if presenter.data_contact.present? json.contact do - json.partial! "api/v1/contributors/show", contributor: presenter.data_contact, + json.partial! 'api/v1/contributors/show', contributor: presenter.data_contact, is_contact: true end end @@ -36,27 +40,29 @@ end unless @minimal if presenter.contributors.any? json.contributor presenter.contributors do |contributor| - json.partial! "api/v1/contributors/show", contributor: contributor, + json.partial! 'api/v1/contributors/show', contributor: contributor, is_contact: false end end if presenter.costs.any? json.cost presenter.costs do |cost| - json.partial! "api/v1/plans/cost", cost: cost + json.partial! 'api/v1/plans/cost', cost: cost end end json.project [plan] do |pln| - json.partial! "api/v1/plans/project", plan: pln + json.partial! 'api/v1/plans/project', plan: pln end - json.dataset [plan] do |dataset| - json.partial! "api/v1/datasets/show", plan: plan, dataset: dataset + outputs = plan.research_outputs.any? ? plan.research_outputs : [plan] + + json.dataset outputs do |output| + json.partial! "api/v1/datasets/show", output: output end json.extension [plan.template] do |template| - json.set! ApplicationService.application_name.split("-").first.to_sym do + json.set! :dmproadmap do json.template do json.id template.id json.title template.title diff --git a/app/views/api/v1/plans/index.json.jbuilder b/app/views/api/v1/plans/index.json.jbuilder index 48d9ef740f..590dab179f 100644 --- a/app/views/api/v1/plans/index.json.jbuilder +++ b/app/views/api/v1/plans/index.json.jbuilder @@ -1,9 +1,9 @@ # frozen_string_literal: true -json.partial! "api/v1/standard_response", total_items: @total_items +json.partial! 'api/v1/standard_response', total_items: @total_items json.items @items do |item| json.dmp do - json.partial! "api/v1/plans/show", plan: item + json.partial! 'api/v1/plans/show', plan: item end end diff --git a/app/views/api/v1/templates/index.json.jbuilder b/app/views/api/v1/templates/index.json.jbuilder index ba5991f3d8..99d4af8662 100644 --- a/app/views/api/v1/templates/index.json.jbuilder +++ b/app/views/api/v1/templates/index.json.jbuilder @@ -1,6 +1,6 @@ # frozen_string_literal: true -json.partial! "api/v1/standard_response", total_items: @total_items +json.partial! 'api/v1/standard_response', total_items: @total_items json.items @items do |template| presenter = Api::V1::TemplatePresenter.new(template: template) @@ -13,13 +13,13 @@ json.items @items do |template| json.modified template.updated_at.to_formatted_s(:iso8601) json.affiliation do - json.partial! "api/v1/orgs/show", org: template.org + json.partial! 'api/v1/orgs/show', org: template.org end json.template_id do identifier = Api::V1::ConversionService.to_identifier(context: @application, value: template.id) - json.partial! "api/v1/identifiers/show", identifier: identifier + json.partial! 'api/v1/identifiers/show', identifier: identifier end end end diff --git a/app/views/contact_us/contacts/_new_right.html.erb b/app/views/contact_us/contacts/_new_right.html.erb index a5c85c4944..ecf4be41b4 100644 --- a/app/views/contact_us/contacts/_new_right.html.erb +++ b/app/views/contact_us/contacts/_new_right.html.erb @@ -7,7 +7,7 @@ Rails.configuration.x.organisation.address.fetch(:line4, ""), Rails.configuration.x.organisation.address.fetch(:country, "")].compact.each do |addr_line| %> <% if addr_line.present? %> - <%= addr_line %>
          + <%= addr_line.is_a?(Array) ? addr_line.join(' ') : addr_line %>
          <% end %> <% end %> diff --git a/app/views/contact_us/contacts/new.html.erb b/app/views/contact_us/contacts/new.html.erb index 7ec5d7cf1f..206a09c033 100644 --- a/app/views/contact_us/contacts/new.html.erb +++ b/app/views/contact_us/contacts/new.html.erb @@ -6,8 +6,8 @@ <%= sanitize _('%{application_name} is provided by the %{organisation_name}.
          You can find out more about us on our website (new window)%{open_in_new_window_text}. If you would like to contact us about %{application_name}, please fill out the form below.') % { organisation_name: _(Rails.configuration.x.organisation.name), organisation_url: Rails.configuration.x.organisation.url, - application_name: ApplicationService.application_name, - open_in_new_window_text: _('Opens in new window') }, + application_name: _(ApplicationService.application_name), + open_in_new_window_text: 'Opens in new window' }, tags: %w( a br span em ) %>

          diff --git a/app/views/contributors/_form.html.erb b/app/views/contributors/_form.html.erb index 52f39137a4..e848cc8cd6 100644 --- a/app/views/contributors/_form.html.erb +++ b/app/views/contributors/_form.html.erb @@ -10,7 +10,7 @@ roles_tooltip = _("Select each role that applies to the contributor.") <%= form.label(:name, _("Name"), class: "control-label") %>
          - <%= form.text_field :name, class: "form-control", spellcheck: true, aria: { required: true } %> + <%= form.text_field :name, class: "form-control", spellcheck: true %>
          @@ -19,7 +19,7 @@ roles_tooltip = _("Select each role that applies to the contributor.") <%= form.label(:email, _("Email"), class: "control-label") %>
          - <%= form.email_field :email, class: "form-control", aria: { required: true } %> + <%= form.email_field :email, class: "form-control" %>
          @@ -38,19 +38,21 @@ roles_tooltip = _("Select each role that applies to the contributor.") <% end %> -
          -
          - <%= form.label(:phone, _("Phone number"), class: "control-label") %> -
          -
          - <%= phone_tooltip %> - <%= form.phone_field :phone, class: "form-control", - title: phone_tooltip, - data: { toggle: "tooltip" }, - pattern: "[0-9\-\.\(\)\+]+", - placeholder: "123-123-1234" %> +<% if Rails.configuration.x.application.display_contributor_phone_number %> +
          +
          + <%= form.label(:phone, _("Phone number"), class: "control-label") %> +
          +
          + <%= phone_tooltip %> + <%= form.phone_field :phone, class: "form-control", + title: phone_tooltip, + data: { toggle: "tooltip" }, + pattern: "[0-9\-\.\(\)\+]+", + placeholder: "123-123-1234" %> +
          -
          +<% end %>
          @@ -59,7 +61,6 @@ roles_tooltip = _("Select each role that applies to the contributor.") orgs: orgs, default_org: contributor.org, required: false, - orgs: @orgs, label: _("Affiliation") } %>
          diff --git a/app/views/devise/invitations/edit.html.erb b/app/views/devise/invitations/edit.html.erb index c10d495d4b..586af8dff6 100644 --- a/app/views/devise/invitations/edit.html.erb +++ b/app/views/devise/invitations/edit.html.erb @@ -32,9 +32,9 @@ <%= render partial: @org_partial, locals: { form: f, + orgs: @all_orgs, default_org: nil, - required: true, - orgs: @all_orgs + required: true } %>
          <%= f.button(_('Create account'), class: "btn btn-default", type: "submit") %> diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb index 3708a9d3a5..b8b8c61cfb 100644 --- a/app/views/devise/mailer/confirmation_instructions.html.erb +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -1,5 +1,7 @@ -

          <%= _("Welcome to %{application_name}") % {application_name: ApplicationService.application_name} %>, <%= @email %>!

          +<% I18n.with_locale I18n.default_locale do %> +

          <%= _("Welcome to %{application_name}") % {application_name: ApplicationService.application_name} %>, <%= @email %>!

          -

          <%= _("Thank you for registering. Please confirm your email address") %>:

          +

          <%= _("Thank you for registering. Please confirm your email address") %>:

          -

          <%= link_to _("Click here to confirm your account"), confirmation_url(@resource, :confirmation_token => @token) %> (<%= _("or copy") %> <%= confirmation_url(@resource, :confirmation_token => @token) %> <%= _("into your browser") %>).

          +

          <%= link_to _("Click here to confirm your account"), confirmation_url(@resource, :confirmation_token => @token) %> (<%= _("or copy") %> <%= confirmation_url(@resource, :confirmation_token => @token) %> <%= _("into your browser") %>).

          +<% end %> diff --git a/app/views/devise/mailer/invitation_instructions.html.erb b/app/views/devise/mailer/invitation_instructions.html.erb index 874fd9bc9d..1e1af1af3e 100644 --- a/app/views/devise/mailer/invitation_instructions.html.erb +++ b/app/views/devise/mailer/invitation_instructions.html.erb @@ -1,14 +1,21 @@ <% tool_name = ApplicationService.application_name link = accept_invitation_url(@resource, :invitation_token => @token) - helpdesk_email = Rails.configuration.x.organisation.helpdesk_email contact_us = (Rails.configuration.x.organisation.contact_us_url || contact_us_url) email_subject = _('Query or feedback related to %{tool_name}') %{ :tool_name => tool_name } user_name = User.find_by(email: @resource.email).nil? ? @resource.email : User.find_by(email: @resource.email).name(false) - inviter_name = @resource.invited_by.name + inviter = @resource.invited_by + inviter_name = inviter.name + helpdesk_email = inviter.org&.helpdesk_email || + Rails.configuration.x.organisation.helpdesk_email %> +<% I18n.with_locale I18n.default_locale do %> +

          + + +

          - <%= _('Hello %{user_name}') %{ :user_name => user_name } %> + <%= _("Hello %{user_name}") %{ :user_name => user_name } %>

          <%= _("Your colleague %{inviter_name} has invited you to contribute to "\ @@ -28,9 +35,11 @@ <%= _('The %{tool_name} team') %{:tool_name => tool_name} %>

          + <%= _('Please do not reply to this email.') %>  <%= sanitize(_('If you have any questions or need help, please contact us at %{helpdesk_email} or visit %{contact_us_url}') % { helpdesk_email: mail_to(helpdesk_email, helpdesk_email, subject: email_subject), contact_us_url: link_to(contact_us, contact_us) }) %>

          +<% end %> diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb index a35b4811c7..b5c41e5b85 100644 --- a/app/views/devise/mailer/reset_password_instructions.html.erb +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -3,21 +3,25 @@ helpdesk_email = Rails.configuration.x.organisation.helpdesk_email contact_us = Rails.configuration.x.organisation.contact_us_url || contact_us_url email_subject = _('Query or feedback related to %{tool_name}') %{ :tool_name => tool_name } + user = User.find_by(email: @resource.email) + helpdesk_email = user.org&.helpdesk_email || + Rails.configuration.x.organisation.helpdesk_email %> - -

          - <%= _('Hello %{user_email}') %{ :user_email => @resource.email } %> -

          -

          - <%= _('Someone has requested a link to change your %{tool_name} password. You can do this through the link below.') %{ :tool_name => tool_name } %> -

          -

          <%= link_to _('Change my password'), edit_password_url(@resource, :reset_password_token => @token) %>

          -

          <%= _("If you didn't request this, please ignore this email.") %>

          -

          - <%= _('All the best') %> -
          - <%= _('The %{tool_name} team') %{:tool_name => tool_name} %> -

          -

          - <%= sanitize(_('If you have any questions or need help, please contact us at %{helpdesk_email} or visit %{contact_us}') %{ :helpdesk_email => mail_to(helpdesk_email, helpdesk_email, subject: email_subject), :contact_us => link_to(contact_us, contact_us) }) %> -

          +<% I18n.with_locale I18n.default_locale do %> +

          + <%= _('Hello %{user_email}') %{ :user_email => @resource.email } %> +

          +

          + <%= _('Someone has requested a link to change your %{tool_name} password. You can do this through the link below.') %{ :tool_name => tool_name } %> +

          +

          <%= link_to _('Change my password'), edit_password_url(@resource, :reset_password_token => @token) %>

          +

          <%= _("If you didn't request this, please ignore this email.") %>

          +

          + <%= _('All the best') %> +
          + <%= _('The %{tool_name} team') %{:tool_name => tool_name} %> +

          +

          + <%= _('Please do not reply to this email.') %> <%= sanitize(_('If you have any questions or need help, please contact us at %{helpdesk_email} or visit %{contact_us}') %{ :helpdesk_email => mail_to(helpdesk_email, helpdesk_email, subject: email_subject), :contact_us => link_to(contact_us, contact_us) }) %> +

          +<% end %> diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb index a7d19a5569..d536f0d174 100644 --- a/app/views/devise/mailer/unlock_instructions.html.erb +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -1,9 +1,9 @@ +<% I18n.with_locale I18n.default_locale do %> +

          <%= _("Hello") %> <%= @resource.email %>!

          -

          <%= _("Hello") %> <%= @resource.email %>!

          +

          <%= _("Your") %> <%= link_to ApplicationService.application_name, root_url %> <%= _("account has been locked due to an excessive number of unsuccessful sign in attempts.") %>

          -

          <%= _("Your") %> <%= link_to ApplicationService.application_name, root_url %> <%= _("account has been locked due to an excessive number of unsuccessful sign in attempts.") %>

          - -

          <%= _("Click the link below to unlock your account") %>:

          - -

          <%= link_to _("Unlock my account"), unlock_url(@resource, :unlock_token => @token) %>

          +

          <%= _("Click the link below to unlock your account") %>:

          +

          <%= link_to _("Unlock my account"), unlock_url(@resource, :unlock_token => @token) %>

          +<% end %> diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb index a1db6c29f8..b658a735bd 100644 --- a/app/views/devise/passwords/edit.html.erb +++ b/app/views/devise/passwords/edit.html.erb @@ -11,19 +11,19 @@ <%= devise_error_messages! %> <%= f.hidden_field :reset_password_token %> -
          - <%= f.label(:password, _('New password'), class: 'control-label') %> - <%= f.password_field(:password, class: 'form-control', "aria-required": true) %> -
          -
          - <%= f.label(:password_confirmation, _('Password confirmation'), class: 'control-label') %> - <%= f.password_field(:password_confirmation, class: 'form-control', "aria-required": true) %> -
          -
          - -
          +
          + <%= f.label(:password, _('New password'), class: 'control-label') %> + <%= f.password_field(:password, class: 'form-control', "aria-required": true) %> +
          +
          + <%= f.label(:password_confirmation, _('Password confirmation'), class: 'control-label') %> + <%= f.password_field(:password_confirmation, class: 'form-control', "aria-required": true) %> +
          +
          + +
          <% if Rails.configuration.x.recaptcha.enabled %>
          diff --git a/app/views/devise/registrations/_personal_details.html.erb b/app/views/devise/registrations/_personal_details.html.erb index 28b24adb94..6e2ab54c67 100644 --- a/app/views/devise/registrations/_personal_details.html.erb +++ b/app/views/devise/registrations/_personal_details.html.erb @@ -29,8 +29,7 @@ form: f, orgs: orgs, default_org: current_user.org, - required: true, - orgs: @orgs + required: true } %>
          <% if org_admin %> @@ -67,9 +66,7 @@ <% end %> - <% @identifier_schemes.each do |scheme| %> - <% next if scheme.name.downcase == 'shibboleth' && !Rails.application.config.shibboleth_enabled %>
          <% if scheme.name.downcase == 'shibboleth' %>
        • - <% unless FeatureFlagHelper.enabled?(:on_sandbox) %> - - <% end %> - <% if @user.can_org_admin? %> + + <% if @user.can_use_api? %>
        diff --git a/app/views/paginable/research_outputs/_index.html.erb b/app/views/paginable/research_outputs/_index.html.erb new file mode 100644 index 0000000000..8b89fb7163 --- /dev/null +++ b/app/views/paginable/research_outputs/_index.html.erb @@ -0,0 +1,65 @@ +<%# locals: @plan, scope %> + + + + + + + + + + <% if @plan.administerable_by?(current_user.id) %> + + <% end %> + + + + <% scope.each do |output| %> + <% + presenter = ResearchOutputPresenter.new(research_output: output) + rdate = presenter.display_release + %> + + + + + + + <% if @plan.administerable_by?(current_user.id) %> + + <% end %> + + <% end %> + +
        + <%= _("Title") %> <%= paginable_sort_link("research_outputs.title") %> + + <%= _("Type") %> <%= paginable_sort_link("research_outputs.output_type") %> + + <%= _("Repository") %> + + <%= _("Release date") %> <%= paginable_sort_link("research_outputs.release_date") %> + + <%= _("Access level") %> + + <%= _("Actions") %> +
        <%= presenter.display_name %><%= presenter.display_type %><%= presenter.display_repository.join("
        ").html_safe %>
        <%= rdate.is_a?(Date) ? l(rdate, formats: :short) : rdate %><%= presenter.display_access %> + +
        diff --git a/app/views/paginable/templates/_publicly_visible.html.erb b/app/views/paginable/templates/_publicly_visible.html.erb index 7927bf8511..0a0af28673 100644 --- a/app/views/paginable/templates/_publicly_visible.html.erb +++ b/app/views/paginable/templates/_publicly_visible.html.erb @@ -43,7 +43,7 @@ <% if signed_in %> - <%= direct_link(template, false, nil, nil, Rails.application.config.direct_link_protocol) %> + <%= direct_link(template) %> <% else %> <%= _('Requires login') %> diff --git a/app/views/phases/_edit_plan_answers.html.erb b/app/views/phases/_edit_plan_answers.html.erb index e25718e655..df53921eb3 100644 --- a/app/views/phases/_edit_plan_answers.html.erb +++ b/app/views/phases/_edit_plan_answers.html.erb @@ -58,16 +58,25 @@ - <% section.questions.includes([:question_format]).each_with_index do |question, i| %> + <% section.questions.each_with_index do |question, i| %> <% # Load the answer or create a new one answer = answers[question.id] if plan.present? if answer.blank? answer = Answer.new({ plan: plan, question: question }) end %> -
        -
        +
        +
        + <% + guidance_comments_opened_by_default = Rails.configuration.x.application.guidance_comments_opened_by_default + %> + <% if Rails.configuration.x.application.guidance_comments_toggleable %> +
        + + <%= _('Comments & Guidance') %> +
        + <% end %>
        " class="answer-locking"> @@ -87,7 +96,8 @@
        -
        + <% style = guidance_comments_opened_by_default ? '' : 'display: none' %> +
        <%= render partial: '/phases/guidances_notes', locals: { plan: plan, diff --git a/app/views/plans/_download_form.html.erb b/app/views/plans/_download_form.html.erb index fd062c643d..930d9b6a04 100644 --- a/app/views/plans/_download_form.html.erb +++ b/app/views/plans/_download_form.html.erb @@ -1,3 +1,6 @@ +<% + download_coversheet_tickbox_checked = Rails.configuration.x.plans.download_coversheet_tickbox_checked || false +%> <%= form_tag(plan_export_path(@plan), method: :get, target: '_blank', id: 'download_form') do |f| %>

        <%= _('Format') %>

        @@ -23,7 +26,7 @@ <%= _("Optional plan components") %>
        <%= label_tag 'export[project_details]' do %> - <%= check_box_tag 'export[project_details]', true, true %> + <%= check_box_tag 'export[project_details]', true, download_coversheet_tickbox_checked %> <%= _('project details coversheet') %> <% end %>
        @@ -39,6 +42,14 @@ <%= _('unanswered questions') %> <% end %>
        + <% if @plan.research_outputs.any? %> +
        + <%= label_tag 'export[research_outputs]' do %> + <%= check_box_tag 'export[research_outputs]', true, true %> + <%= _('research outputs') %> + <% end %> +
        + <% end %> <% if @plan.template.customization_of.present? %>
        <%= label_tag 'export[custom_sections]' do %> diff --git a/app/views/plans/_navigation.html.erb b/app/views/plans/_navigation.html.erb index 8ff5b7d613..468321c9de 100644 --- a/app/views/plans/_navigation.html.erb +++ b/app/views/plans/_navigation.html.erb @@ -17,6 +17,12 @@ <% end %> + <% if Rails.configuration.x.madmp.enable_research_outputs %> +
      • "> + <%= link_to _("Research Outputs"), plan_research_outputs_path(plan), role: "tab", + aria: { controls: "content" } %> +
      • + <% end %> <% if plan.administerable_by?(current_user.id) || (current_user.can_org_admin? && current_user.org.plans.include?(plan)) %>
        - <% funding_statuses = Plan::FUNDING_STATUS.map { |status| [_(status[0].to_s.capitalize), _(status[0].to_s)] } %> - <%= form.select :funding_status, - options_for_select(funding_statuses, form.object.funding_status), - { - include_blank: _("- Please select one -") - }, - { class: "form-control" } %> + <% funding_statuses = Plan::FUNDING_STATUS.map { |status| [_(status[0].to_s.capitalize), _(status[0].to_s)] } %> + <%= form.select :funding_status, options_for_select(funding_statuses, form.object.funding_status), + { + include_blank: _("- Please select one -"), + selected: form.object.funding_status + }, + { class: "form-control" } %>
        - <%= form.fields_for :grant, plan.grant do |grant_fields| %>
        <%= grant_fields.label(:value, _("Grant number/url"), class: "control-label") %> diff --git a/app/views/plans/_share_form.html.erb b/app/views/plans/_share_form.html.erb index 613a8db2f0..454ca433de 100644 --- a/app/views/plans/_share_form.html.erb +++ b/app/views/plans/_share_form.html.erb @@ -6,7 +6,7 @@ <% permissions_tooltip = _('Co-owner: Has admin-rights to the plan (can invite other users, view the plan, answer questions, or comment). Editor: Has edit-rights to the plan (can view the plan, answer questions, or comment). Read Only: Has read-rights to the plan (can view the plan or comment)') %>

        <%= _('Set plan visibility') %>

        -

        <%= _('Public or organisational visibility is intended for finished plans.') %>

        +

        <%= _('Public or organisational visibility is intended for finished plans. You must answer at least %{percentage}%% of the questions to enable these options. Note: test plans are set to private visibility by default.') % { :percentage => Rails.configuration.x.plans.default_percentage_answered } %>

        <% allow_visibility = @plan.visibility_allowed? %> <%= form_with model: @plan, id: "set_visibility" do |f| %> > diff --git a/app/views/plans/export.xml.builder b/app/views/plans/export.xml.builder index e45e8449ed..b7ae35fca6 100644 --- a/app/views/plans/export.xml.builder +++ b/app/views/plans/export.xml.builder @@ -2,9 +2,9 @@ xml.instruct! # rubocop:disable Metrics/BlockLength -xml.plan("id" => @plan.id) do - xml.project(@plan.project.title, "id" => @plan.project.id) - xml.phase(@plan.version.phase.title, "id" => @plan.version.phase.id) +xml.plan('id' => @plan.id) do + xml.project(@plan.project.title, 'id' => @plan.project.id) + xml.phase(@plan.version.phase.title, 'id' => @plan.version.phase.id) details = @exported_plan.admin_details if details.present? @@ -18,22 +18,22 @@ xml.plan("id" => @plan.id) do xml.sections do @exported_plan.sections.each do |section| - xml.section("id" => section.id, "number" => section.number, "title" => section.title) do + xml.section('id' => section.id, 'number' => section.number, 'title' => section.title) do xml.answers do questions = @exported_plan.questions_for_section(section.id) questions.each do |question| - xml.question("id" => question.id, "number" => question.number, - "question_format" => question.question_format) do + xml.question('id' => question.id, 'number' => question.number, + 'question_format' => question.question_format) do q_format = question.question_format xml.question_text question.text answer = @plan.answer(question.id, false) unless answer.nil? - xml.answer("id" => answer.id) do # should add user and date info here - if q_format.title == _("Check box") || q_format.title == _("Multi select box") || - q_format.title == _("Radio buttons") || q_format.title == _("Dropdown") + xml.answer('id' => answer.id) do # should add user and date info here + if q_format.title == _('Check box') || q_format.title == _('Multi select box') || + q_format.title == _('Radio buttons') || q_format.title == _('Dropdown') xml.selections do answer.options.each do |option| - xml.selection(option.text, "id" => option.id, "number" => option.number) + xml.selection(option.text, 'id' => option.id, 'number' => option.number) end end xml.comment_text answer.text if question.option_comment_display == true diff --git a/app/views/plans/new.html.erb b/app/views/plans/new.html.erb index 76147317d6..f67a11ab1e 100644 --- a/app/views/plans/new.html.erb +++ b/app/views/plans/new.html.erb @@ -58,7 +58,7 @@ form: org_fields, id_field: :id, default_org: nil, - orgs: @orgs.filter { |org| org.managed == true }, + orgs: @orgs, required: false } %> <% end %> @@ -128,4 +128,4 @@ <%= link_to _('Cancel'), plans_path, class: 'btn btn-default' %> <% end %>
        -
        +
        \ No newline at end of file diff --git a/app/views/plans/request_feedback.html.erb b/app/views/plans/request_feedback.html.erb index 0878409a09..dedb6f8c81 100644 --- a/app/views/plans/request_feedback.html.erb +++ b/app/views/plans/request_feedback.html.erb @@ -1,4 +1,4 @@ -<% title "#{@plan.title} - Request feedback" %> +<% title _("#{@plan.title} - Request feedback") %>
        diff --git a/app/views/research_outputs/_form.html.erb b/app/views/research_outputs/_form.html.erb new file mode 100644 index 0000000000..4f34ebd62e --- /dev/null +++ b/app/views/research_outputs/_form.html.erb @@ -0,0 +1,191 @@ +<%# locals: form, path, method, plan, research_output %> + +<% +presenter = ResearchOutputPresenter.new(research_output: research_output) + +abbrev_tooltip = _("Your research output abbreviation can be used as a reference when answering this plan's questions.") +personal_data_tooltip = _("Any data that could potentially identify a specific individual. Any information that can be used to distinguish one person from another and can be used for de-anonymizing anonymous data can be considered personally identifiable data. (From https://codata.org/rdm-glossary/personally-identifiable-information/)") +repository_tooltip = _("Repositories preserve, manage, and provide access to many types of digital materials in a variety of formats. Materials in online repositories are curated to enable search, discovery, and reuse. There must be sufficient control for the digital material to be authentic, reliable, accessible and usable on a continuing basis. (From https://codata.org/rdm-glossary/personally-identifiable-information/)") +%> + +<%= form_with model: @research_output, url: path, method: method, local: true do |f| %> +
        +
        + <%= label(:output_type, _("Type"), class: "control-label") %> + "> + <%= f.label(:output_type_description, _("Please describe the output type"), class: "control-label") %> + +
        +
        + + <%= f.select :output_type, options_for_select(presenter.selectable_output_types, f.object.output_type), + { + include_blank: _("- Please select one -"), + selected: (research_output.output_type.present? ? research_output.output_type : "" ) + }, { + class: "form-control", + aria: { required: true }, + data: { remote: true, url: plan_output_type_selection_path(plan), method: :get } + } %> + + "> + <%= f.text_field :output_type_description, class: "form-control", + aria: { required: research_output.other? } %> + +
        +
        + +
        +
        + + <%= f.label(:title, _("Title"), class: "control-label") %> + + + <%= f.label(:abbreviation, _("Abbreviation"), class: "control-label") %> + +
        +
        + + <%= f.text_field :title, class: "form-control", aria: { required: true } %> + + + <%= f.text_field :abbreviation, class: "form-control", title: abbrev_tooltip, + maxlength: 10, data: { toggle: "tooltip" } %> + +
        +
        +
        +
        + <%= f.label(:description, _("Description"), class: "control-label") %> +
        +
        + + <%= f.text_area :description, rows: 4, class: 'form-control tinymce', data: { toggle: "tooltip" } %> + +
        +
        + +
        +
        + + <%= f.label :sensitive_data do %> + <%= f.check_box :sensitive_data, data: { toggle: "tooltip" } %> + <%= _('May contain sensitive data?') %> + <% end %> + + + <%= f.label :personal_data do %> + <%= f.check_box :personal_data, data: { toggle: "tooltip" }, + title: personal_data_tooltip %> + <%= _('May contain personally identifiable information?') %> + <% end %> + +
        +
        + + <% if Rails.configuration.x.madmp.enable_repository_selection && Repository.all.any? %> +
        +
        + +

        <%= label_tag _("Intended repositories") %>

        +
        +
        +
        + +
        + <%= render partial: "layouts/modal_search/selections", + locals: { + namespace: "repositories", + button_label: _("Add a repository"), + item_name_attr: :name, + results: research_output.repositories, + selected: true, + result_partial: "research_outputs/repositories/search_result", + search_path: plan_repository_search_path(research_output.plan), + search_method: :get + } %> +
        + <% end %> + + <% if Rails.configuration.x.madmp.enable_metadata_standard_selection && MetadataStandard.all.any? %> +
        +
        + +

        <%= f.label(:metadata_standard, _("Metadata standards"), class: "control-label") %>

        +
        +
        +
        +
        + <%= render partial: "layouts/modal_search/selections", + locals: { + namespace: "metadata_standards", + button_label: _("Add a metadata standard"), + item_name_attr: :title, + results: research_output.metadata_standards, + selected: true, + result_partial: "research_outputs/metadata_standards/search_result", + search_path: plan_metadata_standard_search_path(research_output.plan), + search_method: :get + } %> +
        + <% end %> + +
        +
        + + <%= f.label(:release_date, _("Anticipated release date"), class: "control-label") %> + <%= f.date_field :release_date, class: "form-control" %> + + + <%= f.label(:access, _("Initial access level"), class: "control-label") %> + <%= f.select :access, options_for_select(presenter.selectable_access_types, f.object.access), + { selected: f.object.access }, + { class: "form-control" } %> + +
        +
        + + <% if Rails.configuration.x.madmp.enable_license_selection && License.all.any? %> +
        + <%= render partial: "research_outputs/licenses/form", + locals: { research_output: research_output, presenter: presenter } %> +
        + <% end %> + +
        "> +
        + + <%= f.label(:byte_size, _("Anticipated file size"), class: "control-label") %> + +
        +
        + + <% file_size = presenter.converted_file_size(size: f.object.byte_size) %> + <%= f.number_field "file_size", min: 1, step: 0.1, value: file_size[:size], class: "form-control" %> + + + <%= f.select "file_size_unit", options_for_select(presenter.selectable_size_units, file_size[:unit]), + { selected: file_size[:unit] }, + { class: "form-control float-left" } %> + +
        +
        + +
        +
        +
        + <%= f.button(_("Save"), class: "btn btn-default", type: "submit") %> + <% unless research_output.new_record? %> + <%= link_to _("Remove"), plan_research_output_path(plan, research_output), + method: :delete, class: "btn btn-default" %> + <% end %> + <%= link_to _("Cancel"), plan_research_outputs_path(plan), + class: "btn btn-default" %> +
        +
        +
        +<% end %> + +<%= render partial: "research_outputs/repositories/search", locals: { research_output: research_output } %> + +<%= render partial: "research_outputs/metadata_standards/search", locals: { research_output: research_output } %> diff --git a/app/views/research_outputs/edit.html.erb b/app/views/research_outputs/edit.html.erb new file mode 100644 index 0000000000..a85d3003cf --- /dev/null +++ b/app/views/research_outputs/edit.html.erb @@ -0,0 +1,28 @@ +<% title "#{@plan.title} - edit research output" %> + +
        +
        + +

        <%= @plan.title %>

        +
        +
        + +<% content_for :plan_tab_body do %> +

        + <%= _("Editing %{research_output_title}") % { research_output_title: @research_output.title } %> + <%= link_to _('View all research outputs'), plan_research_outputs_path(@plan), + class: "btn btn-default pull-right" %> +

        +
        +
        + <%= render partial: "research_outputs/form", + locals: { plan: @plan, + research_output: @research_output, + mime_types: @mime_types, + path: plan_research_output_path(@plan, @research_output), + method: :put } %> +
        +
        +<% end %> + +<%= render partial: "plans/navigation", locals: { plan: @plan } %> diff --git a/app/views/research_outputs/index.html.erb b/app/views/research_outputs/index.html.erb new file mode 100644 index 0000000000..c098c0521d --- /dev/null +++ b/app/views/research_outputs/index.html.erb @@ -0,0 +1,37 @@ +<% title "#{@plan.title} - research outputs" %> + +
        +
        + +

        <%= @plan.title %>

        +
        +
        + +<% content_for :plan_tab_body do %> +
        +
        +

        + <%= _("Please list your anticipated research output(s).") %> +

        +
        + + <% if @research_outputs.any? %> + <%= paginable_renderise partial: "/paginable/research_outputs/index", + controller: "paginable/research_outputs", + action: "index", + scope: @research_outputs, + locals: { plan: @plan }, + query_params: { + sort_field: 'research_outputs.title', + sort_direction: :asc } %> + <% end %> + + <% if @plan.administerable_by?(current_user.id) %> + <%= link_to _("Add a research output"), new_plan_research_output_path(@plan), + class: "btn btn-primary" %> + <% end %> +
        +
        +<% end %> + +<%= render partial: "plans/navigation", locals: { plan: @plan } %> diff --git a/app/views/research_outputs/licenses/_form.html.erb b/app/views/research_outputs/licenses/_form.html.erb new file mode 100644 index 0000000000..a77815de1b --- /dev/null +++ b/app/views/research_outputs/licenses/_form.html.erb @@ -0,0 +1,34 @@ +<%# locals: research_output, presenter %> +<% +default_guidance = "https://choosealicense.com/" + +preferred_licenses = presenter.preferred_licenses +preferred_licenses << ["- Other Licenses - will load a complete list -", "0"] + +preferred_guidance = Rails.configuration.x.madmp.preferred_licenses_guidance_url +preferred_guidance = default_guidance if preferred_guidance.nil? || preferred_guidance.empty? + +selected = research_output.license_id + +# If the user has selected 'Other Licenses' or they have a selection that is not in the preferred list +show_preferred = selected.nil? || (selected != 0 && preferred_licenses.map { |array| array[1] }.include?(selected)) + +options = show_preferred ? preferred_licenses : presenter.complete_licenses +guidance = show_preferred ? preferred_guidance : default_guidance + +args = { include_blank: _("- Please select one -"), selected: selected, class: "form-control" } +args[:data] = { remote: true, url: plan_license_selection_path(research_output.plan), method: :get } if show_preferred +%> +
        + + <%= label_tag _("Initial license"), _("Initial license"), class: "control-label" %> + <%= select_tag "research_output[license_id]", options_for_select(options, selected), args %> + + +
         
        +

        + <%= _("For guidance on selecting a license:") %>
        + <%= guidance %> +

        +
        +
        \ No newline at end of file diff --git a/app/views/research_outputs/metadata_standard_search.js.erb b/app/views/research_outputs/metadata_standard_search.js.erb new file mode 100644 index 0000000000..3ed2079706 --- /dev/null +++ b/app/views/research_outputs/metadata_standard_search.js.erb @@ -0,0 +1,19 @@ +var resultsDiv = $('#modal-search-metadata_standards-results'); + +resultsDiv.html('<%= + escape_javascript( + render( + partial: "layouts/modal_search/results", + locals: { + namespace: "metadata_standards", + results: @search_results, + selected: false, + item_name_attr: :title, + result_partial: "research_outputs/metadata_standards/search_result", + search_path: plan_metadata_standard_search_path(@plan), + search_method: :get + } + ) + ) %>'); + +toggleSpinner(false); \ No newline at end of file diff --git a/app/views/research_outputs/metadata_standards/_search.html.erb b/app/views/research_outputs/metadata_standards/_search.html.erb new file mode 100644 index 0000000000..3aeca77905 --- /dev/null +++ b/app/views/research_outputs/metadata_standards/_search.html.erb @@ -0,0 +1,15 @@ +<%# locals: research_output %> + +<% content_for :"filters-metadata_standards" do %> + +<% end %> + +<%= render partial: "layouts/modal_search/form", + locals: { + namespace: "metadata_standards", + label: "Metadata Standard", + search_examples: "(e.g. DataCite, Dublin, Biological, etc.)", + model_instance: research_output, + search_path: plan_metadata_standard_search_path(research_output.plan), + search_method: :get + } %> diff --git a/app/views/research_outputs/metadata_standards/_search_result.html.erb b/app/views/research_outputs/metadata_standards/_search_result.html.erb new file mode 100644 index 0000000000..d2560fc903 --- /dev/null +++ b/app/views/research_outputs/metadata_standards/_search_result.html.erb @@ -0,0 +1,14 @@ +<%# locals: result %> + +<% if result.present? %> + <%= hidden_field_tag "research_output[metadata_standards_attributes[#{result.id}][id]]", result.id %> + +

        <%= sanitize(result.description) %>

        + + <% website = result.locations.select { |loc| loc["type"] == "website" }.first %> + <% if website.present? %> +
        + <%= link_to website["url"], website["url"], target: "_blank", class: "has-new-window-popup-info" %> +
        + <% end %> +<% end %> \ No newline at end of file diff --git a/app/views/research_outputs/new.html.erb b/app/views/research_outputs/new.html.erb new file mode 100644 index 0000000000..4f461f01f4 --- /dev/null +++ b/app/views/research_outputs/new.html.erb @@ -0,0 +1,28 @@ +<% title "#{@plan.title} - add contributor" %> + +
        +
        + +

        <%= @plan.title %>

        +
        +
        + +<% content_for :plan_tab_body do %> +

        + <%= _("New research output") %> + <%= link_to _('View all research outputs'), plan_research_outputs_path(@plan), + class: "btn btn-default pull-right" %> +

        +
        +
        + <%= render partial: "research_outputs/form", + locals: { plan: @plan, + research_output: @research_output, + mime_types: @mime_types, + path: plan_research_outputs_path, + method: :post } %> +
        +
        +<% end %> + +<%= render partial: "plans/navigation", locals: { plan: @plan } %> diff --git a/app/views/research_outputs/repositories/_search.html.erb b/app/views/research_outputs/repositories/_search.html.erb new file mode 100644 index 0000000000..e83facca88 --- /dev/null +++ b/app/views/research_outputs/repositories/_search.html.erb @@ -0,0 +1,36 @@ +<%# locals: research_output %> + +<% content_for :"filters-repositories" do %> + <% + by_type_tooltip = _("Refine your search to discipline specific, institutional or generalist repositories. When the use of domain-specific repositories is not available, generalist repositories are a good option and accept all types of data regardless of type, format, content, or discipline.") + by_subject_tooltip = _("Select a subject area to refine your search.") + %> + + + <%= select_tag :"research_output[subject_filter]", + options_for_select(ResearchOutputPresenter.selectable_subjects), + include_blank: _("- Select a subject area -"), + class: "form-control", + title: by_subject_tooltip, + data: { toggle: "tooltip", placement: "bottom" } %> + + + + <%= select_tag :"research_output[type_filter]", + options_for_select(ResearchOutputPresenter.selectable_repository_types), + include_blank: _("- Select a repository type -"), + class: "form-control", + title: by_type_tooltip, + data: { toggle: "tooltip", placement: "bottom" } %> + +<% end %> + +<%= render partial: "layouts/modal_search/form", + locals: { + namespace: "repositories", + label: "Repository", + search_examples: "(e.g. DNA, titanium, FAIR, etc.)", + model_instance: research_output, + search_path: plan_repository_search_path(research_output.plan), + search_method: :get + } %> diff --git a/app/views/research_outputs/repositories/_search_result.html.erb b/app/views/research_outputs/repositories/_search_result.html.erb new file mode 100644 index 0000000000..3db62485e5 --- /dev/null +++ b/app/views/research_outputs/repositories/_search_result.html.erb @@ -0,0 +1,81 @@ +<%# locals: result + :result - the object we will be rending (e.g. instance of a model, a hash, etc.) +%> + +<% if result.present? %> + <%= hidden_field_tag "research_output[repositories_attributes[#{result.id}][id]]", result.id %> + +

        <%= result.description %>

        + + <% unless selected %> +
        + <% line_breaker = 0 %> + <% result.info.fetch("keywords", []).each do |tag| %> + <% if line_breaker >= 50 %> + <% line_breaker = 0 %> + <% end %> + <% line_breaker += tag.length %> +
        + <%= link_to tag, search_path, class: "facet", + title: _("Click to view repositories related to %{subject}") % { subject: tag }, + data: { + remote: true, + params: { research_output: { search_term: tag } }.to_param, + method: search_method + } %> +
        + <% end %> +
        + <% end %> + +
        + <%= link_to _("More info"), "#", class: "more-info-link" %> + + +
        +<% end %> \ No newline at end of file diff --git a/app/views/research_outputs/repositories/_search_result_modified.html.erb b/app/views/research_outputs/repositories/_search_result_modified.html.erb new file mode 100644 index 0000000000..27ab921385 --- /dev/null +++ b/app/views/research_outputs/repositories/_search_result_modified.html.erb @@ -0,0 +1,117 @@ +<%# locals: result + :result - the object we will be rending (e.g. instance of a model, a hash, etc.) +%> + +<% if result.present? %> + <%= hidden_field_tag "research_output[repositories_attributes[#{result.id}][id]]", result.id %> + +

        <%= result.description %>

        + + Parsing result.info +

        <%= result.info %>

        +

        <%= result.info.class %>

        + + + <% + if result.info.is_a? String + new_s = JSON.parse result.info.gsub('=>', ':') + else + new_s = "" + end + %> + + + + <% unless selected %> +
        + <% line_breaker = 0 %> + <% if result.info.is_a? String then keywords = new_s else keywords = result.info end%> + <% keywords.fetch("keywords", []).each do |tag| %> + <% if line_breaker >= 50 %> + <% line_breaker = 0 %> + <% end %> + <% line_breaker += tag.length %> +
        + <%= link_to tag, search_path, class: "facet", + title: _("Click to view repositories related to %{subject}") % { subject: tag }, + data: { + remote: true, + params: { research_output: { search_term: tag } }.to_param, + method: search_method + } %> +
        + <% end %> +
        + <% end %> + +
        +<%= link_to _("More info"), "#", class: "more-info-link" %> + + +
        +<% end %> diff --git a/app/views/research_outputs/repository_search.js.erb b/app/views/research_outputs/repository_search.js.erb new file mode 100644 index 0000000000..56fe7422d0 --- /dev/null +++ b/app/views/research_outputs/repository_search.js.erb @@ -0,0 +1,19 @@ +var resultsDiv = $('#modal-search-repositories-results'); + +resultsDiv.html('<%= + escape_javascript( + render( + partial: "layouts/modal_search/results", + locals: { + namespace: "repositories", + results: @search_results, + selected: false, + item_name_attr: :name, + result_partial: "research_outputs/repositories/search_result", + search_path: plan_repository_search_path(@plan), + search_method: :get + } + ) + ) %>'); + +toggleSpinner(false); diff --git a/app/views/research_outputs/select_license.js.erb b/app/views/research_outputs/select_license.js.erb new file mode 100644 index 0000000000..65938495ad --- /dev/null +++ b/app/views/research_outputs/select_license.js.erb @@ -0,0 +1,15 @@ +var resultsDiv = $('.license-selection'); +<% presenter = ResearchOutputPresenter.new(research_output: @research_output) %> + +resultsDiv.html('<%= + escape_javascript( + render( + partial: "research_outputs/licenses/form", + locals: { + research_output: @research_output, + presenter: presenter + } + ) + ) %>'); + +toggleSpinner(false); diff --git a/app/views/research_outputs/select_output_type.js.erb b/app/views/research_outputs/select_output_type.js.erb new file mode 100644 index 0000000000..65b501a38f --- /dev/null +++ b/app/views/research_outputs/select_output_type.js.erb @@ -0,0 +1,34 @@ +<% presenter = ResearchOutputPresenter.new(research_output: @research_output) %> + +var outputTypeForm = $('.research-output-form'); + +if (outputTypeForm.length > 0) { + <%# Hide or Display the Description text field based on the Output Type selected %> + var outputTypeSpans = outputTypeForm.find('.output-type-description'); + var outputTypeDescription = outputTypeForm.find('#research_output_output_type_description'); + + <% if @research_output.other? %> + outputTypeSpans.removeClass('hidden'); + outputTypeDescription.attr('aria-required', 'true'); + <% else %> + outputTypeDescription.attr('aria-required', 'false'); + outputTypeSpans.addClass('hidden'); + var selected = outputTypeSpans.parent().find('option:selected'); + selected.removeAttr("selected"); + <% end %> + + <%# Hide or Display the Mime Type select box and Byte Size based on the Output Type selected %> + var fileDetailsSection = outputTypeForm.find('.file-details-section'); + + <% if presenter.byte_sizable? %> + fileDetailsSection.removeClass('hidden'); + <% else %> + fileDetailsSection.addClass('hidden'); + var selected = fileDetailsSection.find('option:selected'); + selected.removeAttr("selected"); + var sizeField = fileDetailsSection.find('#research_output_file_size'); + sizeField.val(""); + <% end %> +} + +toggleSpinner(false); diff --git a/app/views/shared/export/_plan.erb b/app/views/shared/export/_plan.erb index 34446355aa..9edeb4ad1c 100644 --- a/app/views/shared/export/_plan.erb +++ b/app/views/shared/export/_plan.erb @@ -58,7 +58,7 @@ <% else %> <%# case where Question has options %> - <% if options.any? %> + <% if options.present? && options.any? %>
          <% options.each do |opt| %>
        • <%= opt.text %>
        • @@ -79,7 +79,7 @@
          <%# case for displaying comments OR text %> <% elsif !blank %> - <%= sanitize answer.text %> + <%= sanitize answer&.text %>

          <% end %> <% end %> @@ -87,7 +87,11 @@ <% end %> <% end %> <% end %> +
        <% end %> + <% end %> + <% if @show_research_outputs %> + <%= render partial: 'shared/export/plan_outputs', locals: { outputs: @plan.research_outputs } %> <% end %> diff --git a/app/views/shared/export/_plan_outputs.erb b/app/views/shared/export/_plan_outputs.erb new file mode 100644 index 0000000000..28e9a71063 --- /dev/null +++ b/app/views/shared/export/_plan_outputs.erb @@ -0,0 +1,51 @@ +<%# locals: outputs %> + +

        <%= _("Planned Research Outputs") %>

        + +<% outputs.each do |output| %> + <% presenter = ResearchOutputPresenter.new(research_output: output) %> +

        <%= "#{presenter.display_type} - \"#{output.title}\"" %>

        + +

        <%= output.description.html_safe %>

        +<% end %> + +


        + +

        <%= _("Planned research output details") %>

        + + + + + + + + + + + + + + + + <% outputs.each do |output| %> + <% presenter = ResearchOutputPresenter.new(research_output: output) %> + <% size_hash = presenter.converted_file_size(size: output.byte_size) %> + + + + + + + + + + + + + <% end %> + +
        <%= _("Title") %><%= _("Type") %><%= _("Anticipated release date") %><%= _("Initial access level") %><%= _("Intended repository(ies)") %><%= _("Anticipated file size") %><%= _("License") %><%= _("Metadata standard(s)") %><%= _("May contain sensitive data?") %><%= _("May contain PII?") %>
        <%= presenter.display_name %><%= presenter.display_type %><%= presenter.display_release %><%= presenter.display_access %><%= sanitize(presenter.display_repository.join("
        ")) %>
        + <% if size_hash[:size].present? %> + <%= "#{number_with_delimiter(size_hash[:size])} #{size_hash[:unit]&.upcase}" %> + <% end %> + <%= presenter.display_license %><%= sanitize(presenter.display_metadata_standard.join("
        ")) %>
        <%= presenter.display_boolean(value: output.sensitive_data) %><%= presenter.display_boolean(value: output.personal_data) %>
        diff --git a/app/views/shared/export/_plan_styling.erb b/app/views/shared/export/_plan_styling.erb index 4eda615477..4f9a3ec200 100644 --- a/app/views/shared/export/_plan_styling.erb +++ b/app/views/shared/export/_plan_styling.erb @@ -2,11 +2,11 @@ @import 'https://fonts.googleapis.com/css?family=<%= font_face.downcase.include?('times') ? 'Times' : 'Helvetica' %>'; body { - font-family: @font-face; + font-family: <%= font_face %>; font-size: <%= font_size %>; margin: <%= margin %>; } - h1 { + h1 { font-size: 1.5rem; font-weight: bold; padding: 0; @@ -59,5 +59,5 @@ } .bold { font-weight: bold; - } + } \ No newline at end of file diff --git a/app/views/shared/export/_plan_txt.erb b/app/views/shared/export/_plan_txt.erb index a6d7306ea2..7a696b13b9 100644 --- a/app/views/shared/export/_plan_txt.erb +++ b/app/views/shared/export/_plan_txt.erb @@ -1,8 +1,7 @@ <%= "#{@plan.title}" %> <%= "----------------------------------------------------------\n" %> <% if @show_coversheet %> - -<%= @hash[:attribution].many? ? _("Creators: ") : _('Creator:') %> <%= @hash[:attribution].join(', ') %> +<%= Array(@hash[:attribution]).many? ? _("Creators: ") + Array(@hash[:attribution]).join(", ") : _('Creator:') + @hash[:attribution] %> <%# Added contributors to coverage of plans. # Users will see both roles and contributor names if the role is filled %> <%# Roles are ranked by PI -> DM -> PA -> Other (if any) %> diff --git a/app/views/shared/org_selectors/_external_only.html.erb b/app/views/shared/org_selectors/_external_only.html.erb index daf0ef3456..e6c3e61b5d 100644 --- a/app/views/shared/org_selectors/_external_only.html.erb +++ b/app/views/shared/org_selectors/_external_only.html.erb @@ -13,8 +13,8 @@ presenter = OrgSelectionPresenter.new(orgs: [default_org], placeholder = _("Begin typing to see a list of suggestions.") %> -<%= form.label :org_name, label %> -<%= form.text_field :org_name, class: "form-control autocomplete", +<%= form.label :name, label %> +<%= form.text_field :name, class: "form-control autocomplete", placeholder: placeholder, value: presenter.name, spellcheck: true, @@ -43,4 +43,4 @@ placeholder = _("Begin typing to see a list of suggestions.")

        <%= _("A new entry will be created for the organisation you have named above. Please double check that your organisation does not appear in the list in a slightly different form.").html_safe %> -

        +

        \ No newline at end of file diff --git a/app/views/shared/org_selectors/_local_only.html.erb b/app/views/shared/org_selectors/_local_only.html.erb index 28138a261c..f80b032948 100644 --- a/app/views/shared/org_selectors/_local_only.html.erb +++ b/app/views/shared/org_selectors/_local_only.html.erb @@ -8,7 +8,8 @@ label = label || _("Organisation") # Allows the hidden id field to be renamed for instances where there are # multiple org selectors on the same form id_field = id_field || :org_id -presenter = OrgSelectionPresenter.new(orgs: orgs, selection: default_org) +presenter = OrgSelectionPresenter.new(orgs: orgs.select{|org| org.managed == true}, selection: default_org) +# add unmanaged filter to local_only to fix issue275, see PR#276 for explaination placeholder = _("Begin typing to see a list of suggestions.") %> @@ -45,4 +46,4 @@ placeholder = _("Begin typing to see a list of suggestions.")

        <%= _("The name you entered was not one of the listed suggestions!") %> -

        +

        \ No newline at end of file diff --git a/app/views/shared/org_selectors/_text_only.html.erb b/app/views/shared/org_selectors/_text_only.html.erb index b7f8ec7682..1b67343486 100644 --- a/app/views/shared/org_selectors/_text_only.html.erb +++ b/app/views/shared/org_selectors/_text_only.html.erb @@ -1,5 +1,6 @@ <%# ISSUE93: in case the change of plan-text field for funder affecting other pages, -this view is specially created to force saving a new funder without warning message pop-up %> +this view is specially created to force saving a new funder without warning message pop-up. +Updated on ISSUE260: this view will pass empty source. Thus no suggestion will pop-up%> <% # Whether or not the org selection is required @@ -22,10 +23,10 @@ presenter = OrgSelectionPresenter.new(orgs: orgs, selection: default_org)
        <%# sources contains an array of Org names %> -<%= form.hidden_field :org_sources, value: presenter.select_list %> +<%= form.hidden_field :org_sources, value: "[]" %> <%# crosswalk contains an array of hashes that contain the Org name, id, identifiers like ROR and other info used by the OrgSelectionService %> -<%= form.hidden_field :org_crosswalk, value: presenter.crosswalk %> +<%= form.hidden_field :org_crosswalk, value: "[]" %> <%# gets updated with the matching record from crosswalk when the user selects or enters something %> <% if form.object[id_field]&.to_s =~ /[0-9]+/ || form.object[id_field].nil? %> @@ -35,4 +36,4 @@ presenter = OrgSelectionPresenter.new(orgs: orgs, selection: default_org) <% else %> <%= form.hidden_field :org_id, class: "autocomplete-result", autocomplete: "off" %> -<% end %> +<% end %> \ No newline at end of file diff --git a/app/views/static_pages/about_us.html.erb b/app/views/static_pages/about_us.html.erb index 441b5d53a5..dfd2396259 100644 --- a/app/views/static_pages/about_us.html.erb +++ b/app/views/static_pages/about_us.html.erb @@ -74,4 +74,4 @@
        -
        + \ No newline at end of file diff --git a/app/views/static_pages/help.html.erb b/app/views/static_pages/help.html.erb index 1c78beb769..5ff010d71d 100644 --- a/app/views/static_pages/help.html.erb +++ b/app/views/static_pages/help.html.erb @@ -58,4 +58,4 @@

        <%= _("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.") %>

        - + \ No newline at end of file diff --git a/app/views/static_pages/privacy.html.erb b/app/views/static_pages/privacy.html.erb index dcc9b2ed3f..d945cbfe3c 100644 --- a/app/views/static_pages/privacy.html.erb +++ b/app/views/static_pages/privacy.html.erb @@ -38,6 +38,7 @@
      • personalisation of user experience (e.g. provision of relevant templates and guidance for associated organizations)
      • network and information security
      +

      2.2 For all visitors to the Site, administrative information collected will be used:

      • for internal administrative use
      • @@ -89,6 +90,7 @@

        Contact Us

        Should you have any questions or concerns about the collection, use, or disclosure of Personal Information as regards the DMP Assistant, or about this Privacy Policy please contact us at support@portagenetwork.ca.

        The Service Providers reserve the right to suspend use by any party (User) if that party engages in, or is suspected of engaging in, activities that violate applicable information access and privacy laws.

        + diff --git a/app/views/static_pages/termsuse.html.erb b/app/views/static_pages/termsuse.html.erb index d508305c90..505e0c2361 100644 --- a/app/views/static_pages/termsuse.html.erb +++ b/app/views/static_pages/termsuse.html.erb @@ -1,3 +1,4 @@ + <% title _('Terms of Use') %>
        @@ -114,4 +115,4 @@

        This Terms of Use document is available under a Creative Commons Attribution-ShareAlike 4.0 (CC BY-SA 4.0) License.

        -
        \ No newline at end of file + diff --git a/app/views/super_admin/themes/_form.html.erb b/app/views/super_admin/themes/_form.html.erb index ab6e82f945..cd46e069f9 100644 --- a/app/views/super_admin/themes/_form.html.erb +++ b/app/views/super_admin/themes/_form.html.erb @@ -14,7 +14,7 @@ <% unless @theme.new_record? %> <%= link_to(_('Delete'), super_admin_theme_path(@theme), class: 'btn btn-default', rel: 'nofollow', 'data-method': 'delete', - 'data-confirm': _("Are you sure you want to delete the theme \"%{title}\"?") % { title: _(@theme.title) }) %> + 'data-confirm': _("Are you sure you want to delete the theme \"%{title}\"?") % { title: @theme.title }) %> <% end %> <%= link_to(_('Cancel'), super_admin_themes_path, class: 'btn btn-default', role: 'button') %> diff --git a/app/views/super_admin/themes/index.html.erb b/app/views/super_admin/themes/index.html.erb index e2128e5391..77557e6013 100644 --- a/app/views/super_admin/themes/index.html.erb +++ b/app/views/super_admin/themes/index.html.erb @@ -1,5 +1,3 @@ - - <% title _('Themes') %>
        diff --git a/app/views/super_admin/users/edit.html.erb b/app/views/super_admin/users/edit.html.erb index abc4870e33..098af7517a 100644 --- a/app/views/super_admin/users/edit.html.erb +++ b/app/views/super_admin/users/edit.html.erb @@ -109,9 +109,9 @@
        <%= paginable_renderise( - partial: '/paginable/plans/org_admin_other_user', + partial: '/paginable/plans/index', controller: 'paginable/plans', - action: 'org_admin_other_user', + action: 'index', scope: @plans, query_params: { sort_field: 'plans.updated_at', sort_direction: 'desc' }) %>
        diff --git a/app/views/user_mailer/_email_signature.html.erb b/app/views/user_mailer/_email_signature.html.erb index 0c3a675a05..a1b64abd30 100644 --- a/app/views/user_mailer/_email_signature.html.erb +++ b/app/views/user_mailer/_email_signature.html.erb @@ -15,9 +15,9 @@ <% if allow_change_prefs %> <%= _('You may change your notification preferences on your profile page. ') %> <% end %> - + <%= _('Please do not reply to this email.') %> <%= sanitize(_('If you have any questions or need help, please contact us at %{helpdesk_email} or visit %{contact_us}') % { - helpdesk_email: mail_to(helpdesk_email, helpdesk_email, + helpdesk_email: mail_to(@helpdesk_email, @helpdesk_email, subject: email_subject), contact_us: link_to(contact_us, contact_us) }) %> diff --git a/app/views/user_mailer/feedback_notification.html.erb b/app/views/user_mailer/feedback_notification.html.erb index 0d329f196a..ca64b9d4cf 100644 --- a/app/views/user_mailer/feedback_notification.html.erb +++ b/app/views/user_mailer/feedback_notification.html.erb @@ -19,4 +19,4 @@ <%= link_to plan_url(@plan), plan_url(@plan) %>

        -<%= render partial: 'email_signature', locals: { allow_change_prefs: false } %> +<%= render partial: 'email_signature', locals: { allow_change_prefs: false } %> \ No newline at end of file diff --git a/app/views/user_mailer/permissions_change_notification.html.erb b/app/views/user_mailer/permissions_change_notification.html.erb index c4d5d2efc5..5a66cd22a3 100644 --- a/app/views/user_mailer/permissions_change_notification.html.erb +++ b/app/views/user_mailer/permissions_change_notification.html.erb @@ -1,5 +1,5 @@

        - <%= _('Hello %{username}') %{ username: @username } %> + <%= _('Hello %{recepientname}') %{ recepientname: @recepient.name } %>

        <%= _('Your permissions relating to %{plan_title} have changed. You now have %{type} access. This means you can %{placeholder1} %{placeholder2}') % { diff --git a/app/views/user_mailer/welcome_notification.html.erb b/app/views/user_mailer/welcome_notification.html.erb index 1c775b85fd..f13fd01a85 100644 --- a/app/views/user_mailer/welcome_notification.html.erb +++ b/app/views/user_mailer/welcome_notification.html.erb @@ -10,5 +10,5 @@ <%= _('The %{tool_name} team') %{ tool_name: tool_name} %>

        - <%= _('You may change your notification preferences on your profile page.') %> + <%= _('You may change your notification preferences on your profile page.') %> <%= _('Please do not reply to this email.') %>

        diff --git a/app/views/users/_notification_preferences.html.erb b/app/views/users/_notification_preferences.html.erb index ad562d6927..2245570248 100644 --- a/app/views/users/_notification_preferences.html.erb +++ b/app/views/users/_notification_preferences.html.erb @@ -7,9 +7,7 @@ <%= link_to _('Deselect all'), '#', id: 'deselect_all' %>

        -

        - <%= _('All Users') %> -

        +

        <%= _('All Users') %>

        <% end %> -

        - <%= _('DMP owners and co-owners') %> -

        +

        <%= _('DMP owners and co-owners') %>