From b52f7351c3af4b9b1d7d777eb14d8f06369df30a Mon Sep 17 00:00:00 2001 From: briri Date: Tue, 3 Aug 2021 09:18:28 -0700 Subject: [PATCH 01/59] added research outputs to plan Added tests added tests for new external_api services Added test for new presenter change migration classname fixes for rubocop and rspec offerings to the rubocop gods cleanup of unused code and added config for license opts fixed tests cleaned up schema Fix for postgres regex matching Fix for postgres regex matching trying to fix postgres updated config to disable research_outputs by default updated plans_controller to work with new Plan.grant setter method removed debug lines fixed typo in plan model and added dependent destroy on a few associations removed debug lines fixed bug in v1 api due to removed Plan.api_client column updated plan spec factory so that :creator ensures the owner's org matches the plan.org added api_client association back to plans model. Added some Mysql vs Postgres helper methods to the ApplicationRecord base class making rubocop happy fix typo making rubocop happy trying to fix postgres :/ Hopefully fibally fixed postgres Hopefully fibally fixed postgres Hopefully fibally fixed postgres --- .../stylesheets/blocks/_modal_search.scss | 75 ++++ .../stylesheets/variables/_colours.scss | 1 + app/controllers/plan_exports_controller.rb | 12 +- app/controllers/plans_controller.rb | 23 +- .../research_outputs_controller.rb | 214 ++++++++++++ app/javascript/packs/application.js | 2 + app/javascript/src/researchOutputs/form.js | 45 +++ app/javascript/src/utils/modalSearch.js | 39 +++ app/models/application_record.rb | 27 ++ app/models/concerns/acts_as_sortable.rb | 10 +- app/models/license.rb | 54 +++ app/models/metadata_standard.rb | 34 ++ app/models/plan.rb | 10 +- app/models/repository.rb | 56 +++ app/models/research_output.rb | 65 ++-- app/models/user.rb | 2 +- app/policies/research_output_policy.rb | 59 ++++ app/presenters/api/v1/api_presenter.rb | 23 ++ .../api/v1/research_output_presenter.rb | 80 +++++ app/presenters/research_output_presenter.rb | 157 +++++++++ app/services/external_apis/rdamsc_service.rb | 146 ++++++++ app/services/external_apis/re3data_service.rb | 159 +++++++++ app/services/external_apis/spdx_service.rb | 104 ++++++ app/views/api/v1/datasets/_show.json.jbuilder | 92 +++-- app/views/api/v1/plans/_show.json.jbuilder | 6 +- app/views/layouts/modal_search/README.md | 155 +++++++++ app/views/layouts/modal_search/_form.html.erb | 89 +++++ .../layouts/modal_search/_result.html.erb | 37 ++ .../layouts/modal_search/_results.html.erb | 56 +++ .../layouts/modal_search/_selections.html.erb | 35 ++ .../research_outputs/_index.html.erb | 65 ++++ app/views/plans/_download_form.html.erb | 8 + app/views/plans/_navigation.html.erb | 6 + app/views/research_outputs/_form.html.erb | 191 +++++++++++ app/views/research_outputs/edit.html.erb | 28 ++ app/views/research_outputs/index.html.erb | 37 ++ .../research_outputs/licenses/_form.html.erb | 34 ++ .../metadata_standard_search.js.erb | 19 ++ .../metadata_standards/_search.html.erb | 15 + .../_search_result.html.erb | 14 + app/views/research_outputs/new.html.erb | 28 ++ .../repositories/_search.html.erb | 36 ++ .../repositories/_search_result.html.erb | 81 +++++ .../research_outputs/repository_search.js.erb | 19 ++ .../research_outputs/select_license.js.erb | 15 + .../select_output_type.js.erb | 34 ++ app/views/shared/export/_plan.erb | 5 + app/views/shared/export/_plan_coversheet.erb | 4 +- app/views/shared/export/_plan_outputs.erb | 51 +++ config/initializers/_dmproadmap.rb | 37 ++ config/initializers/external_apis/rdamsc.rb | 9 + config/initializers/external_apis/re3data.rb | 9 + config/initializers/external_apis/spdx.rb | 9 + config/routes.rb | 17 + db/migrate/20210729204611_madmp_cleanup.rb | 4 - .../20210802161057_create_repositories.rb | 18 + db/migrate/20210802161108_create_licenses.rb | 15 + ...0210802161120_create_metadata_standards.rb | 18 + db/schema.rb | 121 ++++++- lib/tasks/utils/external_api.rake | 19 ++ spec/factories/licenses.rb | 30 ++ spec/factories/metadata_standards.rb | 42 +++ spec/factories/orgs.rb | 2 +- spec/factories/plans.rb | 6 +- spec/factories/repositories.rb | 43 +++ spec/factories/research_outputs.rb | 38 ++- spec/models/license_spec.rb | 61 ++++ spec/models/metadata_standard_spec.rb | 31 ++ spec/models/plan_spec.rb | 2 +- spec/models/repository_spec.rb | 103 ++++++ spec/models/research_output_spec.rb | 9 +- .../research_output_presenter_spec.rb | 184 ++++++++++ .../external_apis/rdamsc_service_spec.rb | 137 ++++++++ .../external_apis/re3data_service_spec.rb | 320 ++++++++++++++++++ .../external_apis/spdx_service_spec.rb | 98 ++++++ spec/support/helpers/webmocks.rb | 10 + .../v1/datasets/_show.json.jbuilder_spec.rb | 130 ++++++- .../modal_search/_form.html.erb_spec.rb | 109 ++++++ .../modal_search/_result.html.erb_spec.rb | 79 +++++ .../modal_search/_results.html.erb_spec.rb | 86 +++++ .../modal_search/_selections.html.erb_spec.rb | 38 +++ 81 files changed, 4250 insertions(+), 141 deletions(-) create mode 100644 app/assets/stylesheets/blocks/_modal_search.scss create mode 100644 app/controllers/research_outputs_controller.rb create mode 100644 app/javascript/src/researchOutputs/form.js create mode 100644 app/javascript/src/utils/modalSearch.js create mode 100644 app/models/license.rb create mode 100644 app/models/metadata_standard.rb create mode 100644 app/models/repository.rb create mode 100644 app/policies/research_output_policy.rb create mode 100644 app/presenters/api/v1/api_presenter.rb create mode 100644 app/presenters/api/v1/research_output_presenter.rb create mode 100644 app/presenters/research_output_presenter.rb create mode 100644 app/services/external_apis/rdamsc_service.rb create mode 100644 app/services/external_apis/re3data_service.rb create mode 100644 app/services/external_apis/spdx_service.rb create mode 100644 app/views/layouts/modal_search/README.md create mode 100644 app/views/layouts/modal_search/_form.html.erb create mode 100644 app/views/layouts/modal_search/_result.html.erb create mode 100644 app/views/layouts/modal_search/_results.html.erb create mode 100644 app/views/layouts/modal_search/_selections.html.erb create mode 100644 app/views/paginable/research_outputs/_index.html.erb create mode 100644 app/views/research_outputs/_form.html.erb create mode 100644 app/views/research_outputs/edit.html.erb create mode 100644 app/views/research_outputs/index.html.erb create mode 100644 app/views/research_outputs/licenses/_form.html.erb create mode 100644 app/views/research_outputs/metadata_standard_search.js.erb create mode 100644 app/views/research_outputs/metadata_standards/_search.html.erb create mode 100644 app/views/research_outputs/metadata_standards/_search_result.html.erb create mode 100644 app/views/research_outputs/new.html.erb create mode 100644 app/views/research_outputs/repositories/_search.html.erb create mode 100644 app/views/research_outputs/repositories/_search_result.html.erb create mode 100644 app/views/research_outputs/repository_search.js.erb create mode 100644 app/views/research_outputs/select_license.js.erb create mode 100644 app/views/research_outputs/select_output_type.js.erb create mode 100644 app/views/shared/export/_plan_outputs.erb create mode 100644 config/initializers/external_apis/rdamsc.rb create mode 100644 config/initializers/external_apis/re3data.rb create mode 100644 config/initializers/external_apis/spdx.rb create mode 100644 db/migrate/20210802161057_create_repositories.rb create mode 100644 db/migrate/20210802161108_create_licenses.rb create mode 100644 db/migrate/20210802161120_create_metadata_standards.rb create mode 100644 spec/factories/licenses.rb create mode 100644 spec/factories/metadata_standards.rb create mode 100644 spec/factories/repositories.rb create mode 100644 spec/models/license_spec.rb create mode 100644 spec/models/metadata_standard_spec.rb create mode 100644 spec/models/repository_spec.rb create mode 100644 spec/presenters/research_output_presenter_spec.rb create mode 100644 spec/services/external_apis/rdamsc_service_spec.rb create mode 100644 spec/services/external_apis/re3data_service_spec.rb create mode 100644 spec/services/external_apis/spdx_service_spec.rb create mode 100644 spec/views/layouts/modal_search/_form.html.erb_spec.rb create mode 100644 spec/views/layouts/modal_search/_result.html.erb_spec.rb create mode 100644 spec/views/layouts/modal_search/_results.html.erb_spec.rb create mode 100644 spec/views/layouts/modal_search/_selections.html.erb_spec.rb 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/variables/_colours.scss b/app/assets/stylesheets/variables/_colours.scss index 17cc9034aa..c10b0e676c 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/controllers/plan_exports_controller.rb b/app/controllers/plan_exports_controller.rb index 27b98c24c1..e58aaa21f0 100644 --- a/app/controllers/plan_exports_controller.rb +++ b/app/controllers/plan_exports_controller.rb @@ -17,6 +17,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 +26,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 @@ -94,7 +96,8 @@ def show_pdf 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, client: current_user }) render json: "{\"dmp\":#{json}}" end @@ -125,9 +128,10 @@ 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 53874b74fc..c7d6fbdc95 100644 --- a/app/controllers/plans_controller.rb +++ b/app/controllers/plans_controller.rb @@ -263,7 +263,7 @@ def update # appropriate namespace, so org_id represents our funder funder = org_from_params(params_in: attrs, allow_create: true) @plan.funder_id = funder&.id - process_grant(grant_params: plan_params[:grant]) + @plan.grant = plan_params[:grant] attrs.delete(:grant) attrs = remove_org_selection_params(params_in: attrs) @@ -530,26 +530,5 @@ 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 - end # rubocop:enable Metrics/ClassLength diff --git a/app/controllers/research_outputs_controller.rb b/app/controllers/research_outputs_controller.rb new file mode 100644 index 0000000000..67955689bc --- /dev/null +++ b/app/controllers/research_outputs_controller.rb @@ -0,0 +1,214 @@ +# frozen_string_literal: true + +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 + 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 + + # 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 + 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 + + # 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 + + 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 + + # 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/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/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/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/models/application_record.rb b/app/models/application_record.rb index 00e1dc91b6..4c6479440e 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -8,4 +8,31 @@ class ApplicationRecord < ActiveRecord::Base 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 ?)" unless mysql_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 + end diff --git a/app/models/concerns/acts_as_sortable.rb b/app/models/concerns/acts_as_sortable.rb index 637e50f6df..9e9c47e95d 100644 --- a/app/models/concerns/acts_as_sortable.rb +++ b/app/models/concerns/acts_as_sortable.rb @@ -11,12 +11,10 @@ def update_numbers!(ids, parent:) 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 diff --git a/app/models/license.rb b/app/models/license.rb new file mode 100644 index 0000000000..cc8bd067da --- /dev/null +++ b/app/models/license.rb @@ -0,0 +1,54 @@ +# 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 + } + +end diff --git a/app/models/metadata_standard.rb b/app/models/metadata_standard.rb new file mode 100644 index 0000000000..d8a3f1e756 --- /dev/null +++ b/app/models/metadata_standard.rb @@ -0,0 +1,34 @@ +# 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 + + # ================ + # = 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}%")) + } + +end diff --git a/app/models/plan.rb b/app/models/plan.rb index a5fe102007..1b8aa2a2c4 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -20,6 +20,7 @@ # org_id :integer # funder_id :integer # grant_id :integer +# api_client_id :integer # research_domain_id :bigint # funding_status :integer # ethical_issues :boolean @@ -37,6 +38,7 @@ # # fk_rails_... (template_id => templates.id) # fk_rails_... (org_id => orgs.id) +# fk_rails_... (api_client_id => api_clients.id) # fk_rails_... (research_domain_id => research_domains.id) # @@ -84,6 +86,8 @@ class Plan < ApplicationRecord belongs_to :funder, class_name: "Org", optional: true + belongs_to :api_client, optional: true + belongs_to :research_domain, optional: true has_many :phases, through: :template @@ -111,10 +115,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 = # ===================== diff --git a/app/models/repository.rb b/app/models/repository.rb new file mode 100644 index 0000000000..06ffad7588 --- /dev/null +++ b/app/models/repository.rb @@ -0,0 +1,56 @@ +# 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) +# + +class Repository < ApplicationRecord + + # ================ + # = 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}%") + } + +end diff --git a/app/models/research_output.rb b/app/models/research_output.rb index 858ef06d10..8174392e4e 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,13 +19,17 @@ # 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) +# +# Foreign Keys +# +# fk_rails_... (plan_id => plans.id) +# fk_rails_... (license_id => licenses.id) # class ResearchOutput < ApplicationRecord @@ -42,14 +46,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 +70,18 @@ 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 } - [] - end - - def repositories - # scheme = IdentifierScheme.find_by(name: '[name of repository scheme]') - # return [] unless scheme.present? - # identifiers.select { |id| id.identifier_scheme = scheme } - [] - end - - def metadata_standards - # scheme = IdentifierScheme.find_by(name: '[name of openaire 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 resource_types - # scheme = IdentifierScheme.find_by(name: '[name of resource_type 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 end diff --git a/app/models/user.rb b/app/models/user.rb index 5391699faa..fde463e9b6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -143,7 +143,7 @@ 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(?)", search_pattern, search_pattern) diff --git a/app/policies/research_output_policy.rb b/app/policies/research_output_policy.rb new file mode 100644 index 0000000000..8b79ddf0bb --- /dev/null +++ b/app/policies/research_output_policy.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +class ResearchOutputPolicy < ApplicationPolicy + + attr_reader :user, :research_output + + def initialize(user, research_output) + raise Pundit::NotAuthorizedError, _("must be logged in") unless user + + unless research_output.present? + raise Pundit::NotAuthorizedError, _("are not authorized to view that plan") + end + + @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/presenters/api/v1/api_presenter.rb b/app/presenters/api/v1/api_presenter.rb new file mode 100644 index 0000000000..272b3f5d61 --- /dev/null +++ b/app/presenters/api/v1/api_presenter.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Api + + module 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/research_output_presenter.rb b/app/presenters/api/v1/research_output_presenter.rb new file mode 100644 index 0000000000..851e5837da --- /dev/null +++ b/app/presenters/api/v1/research_output_presenter.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Api + + module V1 + + 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 + + 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 + + end + + end + +end diff --git a/app/presenters/research_output_presenter.rb b/app/presenters/research_output_presenter.rb new file mode 100644 index 0000000000..74f0d007f1 --- /dev/null +++ b/app/presenters/research_output_presenter.rb @@ -0,0 +1,157 @@ +# frozen_string_literal: true + +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) + 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 + + # 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/services/external_apis/rdamsc_service.rb b/app/services/external_apis/rdamsc_service.rb new file mode 100644 index 0000000000..dbadb74a34 --- /dev/null +++ b/app/services/external_apis/rdamsc_service.rb @@ -0,0 +1,146 @@ +# 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 + + 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 + + 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..f37bbc212f --- /dev/null +++ b/app/services/external_apis/re3data_service.rb @@ -0,0 +1,159 @@ +# 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 + 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 + + 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/spdx_service.rb b/app/services/external_apis/spdx_service.rb new file mode 100644 index 0000000000..363d2e5b4e --- /dev/null +++ b/app/services/external_apis/spdx_service.rb @@ -0,0 +1,104 @@ +# 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/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/plans/_show.json.jbuilder b/app/views/api/v1/plans/_show.json.jbuilder index 6f8d226f5f..4448e464ac 100644 --- a/app/views/api/v1/plans/_show.json.jbuilder +++ b/app/views/api/v1/plans/_show.json.jbuilder @@ -51,8 +51,10 @@ unless @minimal 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| diff --git a/app/views/layouts/modal_search/README.md b/app/views/layouts/modal_search/README.md new file mode 100644 index 0000000000..74b5fa2f33 --- /dev/null +++ b/app/views/layouts/modal_search/README.md @@ -0,0 +1,155 @@ +# Modal Search + +This modal search allows your user to search for something, select the results they want and then places those selections on your form so that they are part of the form submission. + +To add it to your page, you must render 2 partials: +1. The first adds a 'selected results' section to your form. +2. The second adds the modal dialog which should be placed #outside# of your form, typically at the bottom of the page. + +You must also define the following: +1. Determine a namespace to use that will be unique on your page. You can add multiple modal searches to your page. Using a unique namespace allows the JS to properly manage this functionality. +2. A controller action to perform a search. You will specify this path and method when rendering the modal partial. +3. A `js.erb` that will use the namespace to replace the `modal-search-[namespace]-results` section of the modal window. +4. A partial that defines how an individual result should be displayed. The display of a search result is up to you, this partial will be used in the modal search results section as well as the selected results section. The 'Select' and 'Remove' links will be managed by the modal search code. +5. (Optional) A partial that contains additional filter/search options. The modal search contains a 'search term' box. You can define additional facets/filters as needed. + +## Define an area to display selections + +As noted above, you must add a call to render the `layouts/modal_search/selections` partial. This should live within your form element so that any selections the user makes within the modal search window are passed back to the server upon form submission. See `views/research_outputs/_form.html.erb` for an example of rendering this section and `controllers/research_outputs_controller.rb` (and `models/research_output.rb`) for an example of how to process the user's selections. + +The contents of this section will be populated by the JS in `app/javascript/src/utils/modalSearch.js` when a user clicks on the 'Select' link next to item's title/name. Once the item appears in this section, a 'Remove' link will appear that allows the user to remove it from this section. + +Example screenshot of selected repositories: +![Screenshot of some repositories selected via a modal search](../../../../docs/screenshots/modal_selections.png) + +Example render of this section: +```ruby +var resultsDiv = $('#modal-search-repositories-results'); + +resultsDiv.html('<%= + escape_javascript( + 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: repository_search_plan_path(research_output.plan), + search_method: :get + } + ) + ) %>'); +``` + +Locals: +- #namespace# - a unique name to identify the modal. This value can be used to match a selected result to a section of the parent page. +- #button_label# - the text for the button that opens the modal search window +- #item_name_attr# - The attribute that contains the title/name of the item. +- #results# - any currently selected items +- #selected# - this should be 'true' here. This will ensure that the 'Remove' link gets displayed for the selected items contained in the results. +- #result_partial# - The partial you have defined to display the item's info +- #search_path# - the path to controller endpoint that will perform the search +- #search_method# - the http method used to perform the search + +## Define the modal dialog + +This should be placed outside any form elements you may have defined on your page because it uses its own form element to process the search. + +To add the modal search to your page you must render the form partial. For example: +```ruby +<%= 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: repository_search_plan_path(research_output.plan), + search_method: :get + } %> +``` + +Locals: +- #namespace# - a unique name to identify the modal. This value can be used to match a selected result to a section of the parent page. +- #label# - the text to display on the modal window. This will be swapped in so that it reads: '[label] search' +- #search_examples# - Helpful text that will appear in the search term box as a placeholder to givethe user some suggestions. +- #model_instance# - An instance of the parent object that the search results will be associated to. (e.g. an instance of ResearchOutput if the user will be searching for a license or repository). This is used to help define the `form_with` on the modal search form. +- #search_path# - the path to controller endpoint that will perform the search +- #search_method# - the http method used to perform the search + +Example of the modal window: +![Screenshot of the modal search dialog for repositories](../../../../docs/screenshots/modal_search.png) + +Note that the 'search term' text field box is added by default. The two select boxes are custom filters. See below for info on defining custom filters. + +Once the user clicks the search button, your controller/action will be called and the `layouts/modal_search/results` partial will be rendered by your `js.erb`. The results will be paginated, so be sure to include `.page(params[:page])`in your controller! + +Example of the `js.erb`: + +For example: +```ruby +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: repository_search_plan_path(@plan), + search_method: :get + } + ) + ) %>'); +``` + +Locals: +- #namespace# - a unique name to identify the modal. This value can be used to match a selected result to a section of the parent page. +- #results# - any currently selected items +- #selected# - this should be 'false' here. This will ensure that the 'Select' link and pagination controls are displayed. +- #item_name_attr# - The attribute that contains the title/name of the item. +- #result_partial# - The partial you have defined to display the item's info +- #search_path# - the path to controller endpoint that will perform the search. +- #search_method# - the http method used to perform the search + +As the user selects results, the JS will move the result from the modal window to the selections sections described above. + +Note that the modal_search results can work with either an ActiveRecord Model or a Hash! + +## Adding additional search criteria + +By default the modal search will only display the 'search term' text field and an 'Apply filters' button. You can add additional custom filters by supplying content to `yield :filters`. In the screenshot above, you can see 2 additional select boxes that allow the user to further refine the search. + +Example definition of the :filters content: +```ruby +<% content_for :filters do %> + <% + by_type_tooltip = _("Refine your search to discipline specific, institutional or generalist repositories.") + 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 %> +``` diff --git a/app/views/layouts/modal_search/_form.html.erb b/app/views/layouts/modal_search/_form.html.erb new file mode 100644 index 0000000000..a9b469c7bf --- /dev/null +++ b/app/views/layouts/modal_search/_form.html.erb @@ -0,0 +1,89 @@ +<%# +This partial is the entry point for adding the modal search dialog to a page. +See the README.md within this directory for more info: + +Locals: + :namespace - a unique name to identify the modal. This value can be used + to match a selected result to a section of the parent page. + :label - the text to display on the modal window. This will be swapped + in so that it reads: '[label] search' + :search_examples - Helpful text that will appear in the search term box as a + placeholder to givethe user some suggestions. + :model_instance - An instance of the parent object that the search results + will be associated to. (e.g. an instance of ResearchOutput + if the user will be searching for a license or repository). + This is used to help define the `form_with` on the modal search form. + :search_path - the path to controller endpoint that will perform the search + :search_method - the http method used to perform the search +%> + +<% +search_examples = search_examples || "" +results = results || [] + +search_placeholder = _("- Enter a search term %{examples} -") % { examples: search_examples} +no_results_msg = _("No results matched your filter criteria.") +%> + + diff --git a/app/views/layouts/modal_search/_result.html.erb b/app/views/layouts/modal_search/_result.html.erb new file mode 100644 index 0000000000..b7c4a5f736 --- /dev/null +++ b/app/views/layouts/modal_search/_result.html.erb @@ -0,0 +1,37 @@ +<%# +This is calledd by the layouts/modal_search/_results.html.erb partial. + +Locals: +:item_name_attr - The attribute that contains the title/name of the item. +:result - an instance of a result (can be either a Model or a Hash) +:selected - indicates whether this item is within the 'selections' (true) + partial or the 'results' (false) partial +:result_partial - The partial you have defined to display the item's info +:search_path - the path to controller endpoint that will perform the search +:search_method - the http method used to perform the search +%> + +<% title = result[item_name_attr] %> + + diff --git a/app/views/layouts/modal_search/_results.html.erb b/app/views/layouts/modal_search/_results.html.erb new file mode 100644 index 0000000000..c237b76644 --- /dev/null +++ b/app/views/layouts/modal_search/_results.html.erb @@ -0,0 +1,56 @@ +<%# +This is the entry point for the results that are rendered by a `js.erb` file. +See the README.md within this directory for more info: + +Locals: +:namespace - a unique name to identify the modal. This value can be used + to match a selected result to a section of the parent page. +:results - the paginated results of the search +:selected - this should be 'false' here to ensure that the 'Select' link and + pagination controls are displayed. +:item_name_attr - The attribute that contains the title/name of the item. +:result_partial - The partial you have defined to display the item's info +:search_path - the path to controller endpoint that will perform the search +:search_method - the http method used to perform the search +%> + +<% +results = results || [] +selected = selected || false +no_results_msg = _("No results matched your filter criteria.") +%> + +<% unless selected %> + <% if results.any? %> + + <% else %> +
<%= no_results_msg %>
+ <% end %> +<% end %> + +<% results.each do |result| %> + <%= render partial: "layouts/modal_search/result", + locals: { + item_name_attr: item_name_attr, + result: result, + selected: selected, + result_partial: result_partial, + search_path: search_path, + search_method: search_method + }%> +
+<% end %> + +<% if results.any? && !selected %> +
 
+
+ + <%= paginate results, remote: true, method: :post %> + +
+<% end %> diff --git a/app/views/layouts/modal_search/_selections.html.erb b/app/views/layouts/modal_search/_selections.html.erb new file mode 100644 index 0000000000..2ace4f1fec --- /dev/null +++ b/app/views/layouts/modal_search/_selections.html.erb @@ -0,0 +1,35 @@ +<%# +This partial is the entry point for displaying the selected results section of +a modal search window. See the README.md within this directory for more info: + +locals: + :namespace - a unique name to identify the modal. This value can be used + to match a selected result to a section of the parent page. + :button_label - the text for the button that opens the modal search window + :item_name_attr - The attribute that contains the title/name of the item. + :results - any currently selected items + :selected - this should be 'true' here. This will ensure that the 'Remove' + link gets displayed for the selected items contained in the results. + :result_partial - The partial you have defined to display the item's info + :search_path - the path to controller endpoint that will perform the search + :search_method - the http method used to perform the search +%> + + +
+
+ <%= button_tag button_label, type: "button", class: "btn btn-default", + data: { toggle: "modal", target: "#modal-search-#{namespace}" } %> +
+
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/plans/_download_form.html.erb b/app/views/plans/_download_form.html.erb index b56382c57b..d4fe9fae91 100644 --- a/app/views/plans/_download_form.html.erb +++ b/app/views/plans/_download_form.html.erb @@ -39,6 +39,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)) %>
    <% 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_coversheet.erb b/app/views/shared/export/_plan_coversheet.erb index df16331646..f245bafa11 100644 --- a/app/views/shared/export/_plan_coversheet.erb +++ b/app/views/shared/export/_plan_coversheet.erb @@ -1,8 +1,10 @@
    -

    <%= @plan.title %>

    +

    <%= _("Plan Overview") %>

    <%= _("A Data Management Plan created using %{application_name}") % { application_name: ApplicationService.application_name } %>


    +

    <%= _("Title: ") %><%= @hash[:title] %>


    + <%# Using tags as the htmltoword gem does not recognise css styles defined %> <%# Allow raw html (==) for plan_attribution as it has tags %>

    <%== plan_attribution(@hash[:attribution]) %>


    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/config/initializers/_dmproadmap.rb b/config/initializers/_dmproadmap.rb index 0d44e72c5f..5f84c3d1d9 100644 --- a/config/initializers/_dmproadmap.rb +++ b/config/initializers/_dmproadmap.rb @@ -207,9 +207,46 @@ class Application < Rails::Application # --------------------------------------------------- # # Machine Actionable / Networked DMP Features (maDMP) # # --------------------------------------------------- # + # Enable/disable functionality on the Project Details tab config.x.madmp.enable_ethical_issues = false config.x.madmp.enable_research_domain = false + # This flag will enable/disable the entire Research Outputs tab. The others below will + # just enable/disable specific functionality on the Research Outputs tab + config.x.madmp.enable_research_outputs = false + config.x.madmp.enable_license_selection = false + config.x.madmp.enable_metadata_standard_selection = false + config.x.madmp.enable_repository_selection = false + + # The following flags will allow the system to include the question and answer in the JSON output + # - questions with a theme equal to 'Preservation' + config.x.madmp.extract_preservation_statements_from_themed_questions = false + # - questions with a theme equal to 'Data Collection' + config.x.madmp.extract_data_quality_statements_from_themed_questions = false + # - questions with a theme equal to 'Ethics & privacy' or 'Storage & security' + config.x.madmp.extract_security_privacy_statements_from_themed_questions = false + + # Specify a list of the preferred licenses types. These licenses will appear in a select + # box on the 'Research Outputs' tab when editing a plan along with the option to select + # 'other'. When 'other' is selected, the user is presented with the full list of licenses. + # + # The licenses will appear in the order you specify here. + # + # Note that the values you enter must match the :identifier field of the licenses table. + # You can use the `%{latest}` markup in place of version numbers if desired. + config.x.madmp.preferred_licenses = [ + "CC-BY-%{latest}", + "CC-BY-SA-%{latest}", + "CC-BY-NC-%{latest}", + "CC-BY-NC-SA-%{latest}", + "CC-BY-ND-%{latest}", + "CC-BY-NC-ND-%{latest}", + "CC0-%{latest}" + ] + # Link to external guidance about selecting one of the preferred licenses. A default + # URL will be displayed if none is provided here. See app/views/research_outputs/licenses/_form + config.x.madmp.preferred_licenses_guidance_url = "https://creativecommons.org/about/cclicenses/" + end end diff --git a/config/initializers/external_apis/rdamsc.rb b/config/initializers/external_apis/rdamsc.rb new file mode 100644 index 0000000000..f90b83aa16 --- /dev/null +++ b/config/initializers/external_apis/rdamsc.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# Credentials for RDA Metadata Standards Catalog (RDAMSC) +# To disable this feature, simply set 'active' to false +Rails.configuration.x.rdamsc.landing_page_url = "http://rdamsc.bath.ac.uk" +Rails.configuration.x.rdamsc.api_base_url = "https://rdamsc.bath.ac.uk/" +Rails.configuration.x.rdamsc.schemes_path = "api2/m" +Rails.configuration.x.rdamsc.thesaurus_path = "api2/thesaurus/concepts" +Rails.configuration.x.rdamsc.active = true diff --git a/config/initializers/external_apis/re3data.rb b/config/initializers/external_apis/re3data.rb new file mode 100644 index 0000000000..1781626039 --- /dev/null +++ b/config/initializers/external_apis/re3data.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# Credentials for minting DOIs via re3data +# To disable this feature, simply set 'active' to false +Rails.configuration.x.re3data.landing_page_url = "https://www.re3data.org/" +Rails.configuration.x.re3data.api_base_url = "https://www.re3data.org/api/v1/" +Rails.configuration.x.re3data.list_path = "repositories" +Rails.configuration.x.re3data.repository_path = "repository/" +Rails.configuration.x.re3data.active = true diff --git a/config/initializers/external_apis/spdx.rb b/config/initializers/external_apis/spdx.rb new file mode 100644 index 0000000000..559915cd45 --- /dev/null +++ b/config/initializers/external_apis/spdx.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# These configuration settings are used to communicate with the SPDX License repository. +# Licenses are loaded via a Rake task and stored in the local :licenses DB table. +# Please refer to: http://spdx.org/licenses/ +Rails.configuration.x.spdx.landing_page_url = "http://spdx.org/licenses/" +Rails.configuration.x.spdx.api_base_url = "https://raw.githubusercontent.com/spdx/license-list-data/" +Rails.configuration.x.spdx.list_path = "master/json/licenses.json" +Rails.configuration.x.spdx.active = true diff --git a/config/routes.rb b/config/routes.rb index 1960a85ec2..d2e53aa8ba 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -125,6 +125,8 @@ resources :contributors, except: %i[show] + resources :research_outputs, except: %i[show] + member do get "answer" get "share" @@ -135,6 +137,17 @@ post "set_test", constraints: { format: [:json] } get "overview" end + + # Ajax endpoint for ResearchOutput.output_type selection + get "output_type_selection", controller: "research_outputs", action: "select_output_type" + + # Ajax endpoint for ResearchOutput.license_id selection + get "license_selection", controller: "research_outputs", action: "select_license" + + # AJAX endpoints for repository search and selection + get :repository_search, controller: "research_outputs" + # AJAX endpoints for metadata standards search and selection + get :metadata_standard_search, controller: "research_outputs" end resources :usage, only: [:index] @@ -217,6 +230,10 @@ resources :contributors, only: %i[index] do get "index/:page", action: :index, on: :collection, as: :index end + # Paginable actions for research_outputs + resources :research_outputs, only: %i[index] do + get "index/:page", action: :index, on: :collection, as: :index + end end # Paginable actions for users resources :users, only: [] do diff --git a/db/migrate/20210729204611_madmp_cleanup.rb b/db/migrate/20210729204611_madmp_cleanup.rb index bfea82c298..ec6bc24d87 100644 --- a/db/migrate/20210729204611_madmp_cleanup.rb +++ b/db/migrate/20210729204611_madmp_cleanup.rb @@ -11,10 +11,6 @@ def change remove_column :research_outputs, :coverage_start remove_column :research_outputs, :coverage_end - # We're going to move towards a different solution allowing multiple api_clients - # to have an interest in a plan - remove_column :plans, :api_client_id - # Remove the old principal_investigator and data_contact fields since they now # live in the contributors table remove_column :plans, :data_contact diff --git a/db/migrate/20210802161057_create_repositories.rb b/db/migrate/20210802161057_create_repositories.rb new file mode 100644 index 0000000000..ccb7f31d20 --- /dev/null +++ b/db/migrate/20210802161057_create_repositories.rb @@ -0,0 +1,18 @@ +class CreateRepositories < ActiveRecord::Migration[5.2] + def change + create_table :repositories do |t| + t.string :name, null: false, index: true + t.text :description, null: false + t.string :homepage, index: true + t.string :contact + t.string :uri, null: false, index: true + t.json :info + t.timestamps + end + + create_table :repositories_research_outputs do |t| + t.belongs_to :research_output + t.belongs_to :repository + end + end +end diff --git a/db/migrate/20210802161108_create_licenses.rb b/db/migrate/20210802161108_create_licenses.rb new file mode 100644 index 0000000000..e11e60e669 --- /dev/null +++ b/db/migrate/20210802161108_create_licenses.rb @@ -0,0 +1,15 @@ +class CreateLicenses < ActiveRecord::Migration[5.2] + def change + create_table :licenses do |t| + t.string :name, null: false + t.string :identifier, null: false, index: true + t.string :uri, null: false, index: true + t.boolean :osi_approved, default: false + t.boolean :deprecated, default: false + t.timestamps + t.index [:identifier, :osi_approved, :deprecated], name: "index_license_on_identifier_and_criteria" + end + + add_reference :research_outputs, :license, foreign_key: true + end +end diff --git a/db/migrate/20210802161120_create_metadata_standards.rb b/db/migrate/20210802161120_create_metadata_standards.rb new file mode 100644 index 0000000000..2afc566b6f --- /dev/null +++ b/db/migrate/20210802161120_create_metadata_standards.rb @@ -0,0 +1,18 @@ +class CreateMetadataStandards < ActiveRecord::Migration[5.2] + def change + create_table :metadata_standards do |t| + t.string :title + t.text :description + t.string :rdamsc_id + t.string :uri + t.json :locations + t.json :related_entities + t.timestamps + end + + create_table :metadata_standards_research_outputs do |t| + t.references :metadata_standard, null: true, index: { name: "metadata_research_outputs_on_metadata" } + t.references :research_output, null: true, index: { name: "metadata_research_outputs_on_ro" } + end + end +end diff --git a/db/schema.rb b/db/schema.rb index e36701b4ce..cd39fcd935 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_07_29_204611) do +ActiveRecord::Schema.define(version: 2021_08_02_161120) do create_table "annotations", id: :integer, force: :cascade do |t| t.integer "question_id" @@ -52,14 +52,20 @@ t.string "description" t.string "homepage" t.string "contact_name" - t.string "contact_email", null: false + t.string "contact_email" t.string "client_id", null: false t.string "client_secret", null: false t.datetime "last_access" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "org_id" - t.index ["name"], name: "index_api_clients_on_name" + t.text "redirect_uri" + t.string "scopes", default: "", null: false + t.boolean "confidential", default: true + t.boolean "trusted", default: false + t.integer "callback_method" + t.string "callback_uri" + t.index ["name"], name: "index_oauth_applications_on_name" end create_table "conditions", id: :integer, force: :cascade do |t| @@ -84,6 +90,7 @@ t.datetime "created_at" t.datetime "updated_at" t.index ["email"], name: "index_contributors_on_email" + t.index ["name", "id", "org_id"], name: "index_contrib_id_and_org_id" t.index ["org_id"], name: "index_contributors_on_org_id" t.index ["plan_id"], name: "index_contributors_on_plan_id" t.index ["roles"], name: "index_contributors_on_roles" @@ -107,6 +114,21 @@ t.integer "phase_id" end + create_table "external_api_access_tokens", force: :cascade do |t| + t.bigint "user_id", null: false + t.string "external_service_name", null: false + t.string "access_token", null: false + t.string "refresh_token" + t.datetime "expires_at" + t.datetime "revoked_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["expires_at"], name: "index_external_api_access_tokens_on_expires_at" + t.index ["external_service_name"], name: "index_external_api_access_tokens_on_external_service_name" + t.index ["user_id", "external_service_name"], name: "index_external_tokens_on_user_and_service" + t.index ["user_id"], name: "index_external_api_access_tokens_on_user_id" + end + create_table "guidance_groups", id: :integer, force: :cascade do |t| t.string "name" t.integer "org_id" @@ -135,6 +157,7 @@ t.string "logo_url" t.string "identifier_prefix" t.integer "context" + t.string "external_service" end create_table "identifiers", id: :integer, force: :cascade do |t| @@ -157,6 +180,37 @@ t.boolean "default_language" end + create_table "licenses", force: :cascade do |t| + t.string "name", null: false + t.string "identifier", null: false + t.string "uri", null: false + t.boolean "osi_approved", default: false + t.boolean "deprecated", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["identifier", "osi_approved", "deprecated"], name: "index_license_on_identifier_and_criteria" + t.index ["identifier"], name: "index_licenses_on_identifier" + t.index ["uri"], name: "index_licenses_on_uri" + end + + create_table "metadata_standards", force: :cascade do |t| + t.string "title" + t.text "description" + t.string "rdamsc_id" + t.string "uri" + t.json "locations" + t.json "related_entities" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "metadata_standards_research_outputs", force: :cascade do |t| + t.bigint "metadata_standard_id" + t.bigint "research_output_id" + t.index ["metadata_standard_id"], name: "metadata_research_outputs_on_metadata" + t.index ["research_output_id"], name: "metadata_research_outputs_on_ro" + end + create_table "notes", id: :integer, force: :cascade do |t| t.integer "user_id" t.text "text" @@ -220,6 +274,8 @@ t.text "feedback_email_msg" t.string "contact_name" t.boolean "managed", default: false, null: false + t.string "api_create_plan_email_subject" + t.text "api_create_plan_email_body" t.index ["language_id"], name: "fk_rails_5640112cab" t.index ["region_id"], name: "fk_rails_5a6adf6bab" end @@ -256,6 +312,7 @@ t.integer "org_id" t.integer "funder_id" t.integer "grant_id" + t.integer "api_client_id" t.datetime "start_date" t.datetime "end_date" t.boolean "ethical_issues" @@ -266,8 +323,9 @@ t.index ["funder_id"], name: "index_plans_on_funder_id" t.index ["grant_id"], name: "index_plans_on_grant_id" t.index ["org_id"], name: "index_plans_on_org_id" + t.index ["research_domain_id"], name: "index_plans_on_fos_id" t.index ["template_id"], name: "index_plans_on_template_id" - t.index ["research_domain_id"], name: "index_plans_on_research_domain_id" + t.index ["api_client_id"], name: "index_plans_on_api_client_id" end create_table "plans_guidance_groups", id: :integer, force: :cascade do |t| @@ -342,6 +400,42 @@ t.integer "super_region_id" end + create_table "related_identifiers", force: :cascade do |t| + t.bigint "identifier_scheme_id" + t.integer "identifier_type", null: false + t.integer "relation_type", null: false + t.bigint "identifiable_id" + t.string "identifiable_type" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "value", null: false + t.index ["identifiable_id", "identifiable_type", "relation_type"], name: "index_relateds_on_identifiable_and_relation_type" + t.index ["identifier_scheme_id"], name: "index_related_identifiers_on_identifier_scheme_id" + t.index ["identifier_type"], name: "index_related_identifiers_on_identifier_type" + t.index ["relation_type"], name: "index_related_identifiers_on_relation_type" + end + + create_table "repositories", force: :cascade do |t| + t.string "name", null: false + t.text "description", null: false + t.string "homepage" + t.string "contact" + t.string "uri", null: false + t.json "info" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["homepage"], name: "index_repositories_on_homepage" + t.index ["name"], name: "index_repositories_on_name" + t.index ["uri"], name: "index_repositories_on_uri" + end + + create_table "repositories_research_outputs", force: :cascade do |t| + t.bigint "research_output_id" + t.bigint "repository_id" + t.index ["repository_id"], name: "index_repositories_research_outputs_on_repository_id" + t.index ["research_output_id"], name: "index_repositories_research_outputs_on_research_output_id" + end + create_table "research_domains", force: :cascade do |t| t.string "identifier", null: false t.string "label", null: false @@ -367,6 +461,8 @@ t.bigint "byte_size" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "license_id" + t.index ["license_id"], name: "index_research_outputs_on_license_id" t.index ["output_type"], name: "index_research_outputs_on_output_type" t.index ["plan_id"], name: "index_research_outputs_on_plan_id" end @@ -424,6 +520,19 @@ t.boolean "filtered", default: false end + create_table "subscriptions", force: :cascade do |t| + t.bigint "plan_id" + t.integer "subscription_types", null: false + t.string "callback_uri" + t.bigint "subscriber_id" + t.string "subscriber_type" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.datetime "last_notified" + t.index ["plan_id"], name: "index_subscriptions_on_plan_id" + t.index ["subscriber_id", "subscriber_type", "plan_id"], name: "index_subscribers_on_identifiable_and_plan_id" + end + create_table "templates", id: :integer, force: :cascade do |t| t.string "title" t.text "description" @@ -523,6 +632,8 @@ t.index ["user_id"], name: "index_users_perms_on_user_id" end + add_foreign_key "annotations", "orgs" + add_foreign_key "annotations", "questions" add_foreign_key "answers", "plans" add_foreign_key "answers", "questions" add_foreign_key "answers", "users" @@ -545,6 +656,8 @@ add_foreign_key "question_options", "questions" add_foreign_key "questions", "question_formats" add_foreign_key "questions", "sections" + add_foreign_key "research_domains", "research_domains", column: "parent_id" + add_foreign_key "research_outputs", "licenses" add_foreign_key "roles", "plans" add_foreign_key "roles", "users" add_foreign_key "sections", "phases" diff --git a/lib/tasks/utils/external_api.rake b/lib/tasks/utils/external_api.rake index 7b363831f5..315dbfea83 100644 --- a/lib/tasks/utils/external_api.rake +++ b/lib/tasks/utils/external_api.rake @@ -2,6 +2,25 @@ # rubocop:disable Metrics/BlockLength, Layout/LineLength namespace :external_api do + desc "Fetch the latest RDA Metadata Standards" + task load_rdamsc_standards: :environment do + p "Fetching the latest RDAMSC metadata standards and updating the metadata_standards table" + ExternalApis::RdamscService.fetch_metadata_standards + end + + desc "Load Repositories from re3data" + task load_re3data_repos: :environment do + p "Fetching the latest re3data repository metadata and updating the repositories table" + p "This can take in excess of 10 minutes to complete ..." + ExternalApis::Re3dataService.fetch + end + + desc "Load Licenses from SPDX" + task load_spdx_licenses: :environment do + p "Fetching the latest SPDX license metadata and updating the licenses table" + ExternalApis::SpdxService.fetch + end + desc "Seed the Research Domain table with Field of Science categories" task add_field_of_science_to_research_domains: :environment do # TODO: If we can identify an external API authority for this information we should switch diff --git a/spec/factories/licenses.rb b/spec/factories/licenses.rb new file mode 100644 index 0000000000..3564d800c5 --- /dev/null +++ b/spec/factories/licenses.rb @@ -0,0 +1,30 @@ +# 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) +# +FactoryBot.define do + factory :license do + name { Faker::Lorem.sentence } + identifier { Faker::Music::PearlJam.unique.song.upcase } + uri { Faker::Internet.unique.url } + osi_approved { [true, false].sample } + deprecated { [true, false].sample } + end +end diff --git a/spec/factories/metadata_standards.rb b/spec/factories/metadata_standards.rb new file mode 100644 index 0000000000..ca8136b37e --- /dev/null +++ b/spec/factories/metadata_standards.rb @@ -0,0 +1,42 @@ +# 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 +# +FactoryBot.define do + factory :metadata_standard do + description { Faker::Lorem.paragraph } + locations do + [ + { type: %w[website document RDFS].sample, url: Faker::Internet.unique.url }, + { type: %w[website document RDFS].sample, url: Faker::Internet.unique.url } + ] + end + related_entities do + [ + { + role: %w[user tool child scheme].sample, + id: "msc:#{Faker::Number.unique.number(digits: 2)}" + }, + { + role: %w[user tool child scheme].sample, + id: "msc:#{Faker::Number.unique.number(digits: 2)}" + } + ] + end + title { Faker::Lorem.unique.sentence } + uri { Faker::Internet.unique.url } + rdamsc_id { "msc:#{Faker::Number.unique.number(digits: 2)}" } + end +end diff --git a/spec/factories/orgs.rb b/spec/factories/orgs.rb index c93f8ae0a0..00ddd82021 100644 --- a/spec/factories/orgs.rb +++ b/spec/factories/orgs.rb @@ -37,7 +37,7 @@ factory :org do name { Faker::Company.unique.name } links { { "org" => [] } } - abbreviation { SecureRandom.hex(4) } + abbreviation { SecureRandom.hex(6) } feedback_enabled { false } region { Region.first || create(:region) } language { Language.default } diff --git a/spec/factories/plans.rb b/spec/factories/plans.rb index 20bfbdd9a3..6e2d771f2b 100644 --- a/spec/factories/plans.rb +++ b/spec/factories/plans.rb @@ -21,6 +21,7 @@ # org_id :integer # funder_id :integer # grant_id :integer +# api_client_id :integer # research_domain_id :bigint # # Indexes @@ -34,6 +35,7 @@ # # fk_rails_... (template_id => templates.id) # fk_rails_... (org_id => orgs.id) +# fk_rails_... (api_client_id => api_clients.id) # fk_rails_... (research_domain_id => research_domains.id) # @@ -60,7 +62,9 @@ end trait :creator do after(:create) do |obj| - obj.roles << create(:role, :creator, user: create(:user, org: create(:org))) + owner = create(:user, org: create(:org)) + obj.roles << create(:role, :creator, user: owner) + obj.update(org: owner.org) end end trait :commenter do diff --git a/spec/factories/repositories.rb b/spec/factories/repositories.rb new file mode 100644 index 0000000000..922a8a47b2 --- /dev/null +++ b/spec/factories/repositories.rb @@ -0,0 +1,43 @@ +# 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_url (homepage) +# index_repositories_on_url (uri) +# +FactoryBot.define do + factory :repository do + name { Faker::Music::PearlJam.song } + description { Faker::Lorem.paragraph } + homepage { Faker::Internet.unique.url } + uri { Faker::Internet.unique.url } + contact { Faker::Internet.email } + info do + { + types: [%w[disciplinary institutional other].sample], + access: %w[closed open restricted].sample, + keywords: [Faker::Lorem.word], + policies: [{ url: Faker::Internet.url, name: Faker::Music::PearlJam.album }], + subjects: ["#{Faker::Number.number(digits: 2)} #{Faker::Lorem.sentence}"], + pid_system: %w[ARK DOI handle].sample, + upload_types: [{ type: Faker::Lorem.word, restriction: Faker::Lorem.word }], + provider_types: [%w[dataProvider serviceProvider].sample] + } + end + end +end diff --git a/spec/factories/research_outputs.rb b/spec/factories/research_outputs.rb index 807ecd8ad4..4f62a881a1 100644 --- a/spec/factories/research_outputs.rb +++ b/spec/factories/research_outputs.rb @@ -4,31 +4,33 @@ # # Table name: research_outputs # -# id :bigint not null, primary key -# abbreviation :string -# access :integer default(0), not null -# byte_size :bigint -# description :text +# id :bigint(8) not null, primary key +# abbreviation :string(255) +# access :integer default("open"), not null +# byte_size :bigint(8) +# description :text(65535) # display_order :integer -# is_default :boolean default("false") -# output_type :integer default(3), not null -# output_type_description :string +# is_default :boolean +# output_type :integer default("dataset"), not null +# output_type_description :string(255) # personal_data :boolean # release_date :datetime # sensitive_data :boolean -# title :string not null +# title :string(255) not null # created_at :datetime not null # updated_at :datetime not null -# mime_type_id :integer +# license_id :bigint(8) # plan_id :integer # # Indexes # +# index_research_outputs_on_license_id (license_id) # index_research_outputs_on_output_type (output_type) # index_research_outputs_on_plan_id (plan_id) # FactoryBot.define do factory :research_output do + license abbreviation { Faker::Lorem.unique.word } access { ResearchOutput.accesses.keys.sample } byte_size { Faker::Number.number } @@ -42,13 +44,15 @@ sensitive_data { [nil, true, false].sample } title { Faker::Music::PearlJam.song } - trait :complete do - after(:create) do |research_output| - # add a license identifier - # add a repository identifier - # add a metadata_standard identifier - # add a resource_type identifier - end + transient do + repositories_count { 1 } + metadata_standards_count { 1 } + end + + after(:create) do |research_output, evaluator| + research_output.repositories = create_list(:repository, evaluator.repositories_count) + research_output.metadata_standards = create_list(:metadata_standard, + evaluator.metadata_standards_count) end end end diff --git a/spec/models/license_spec.rb b/spec/models/license_spec.rb new file mode 100644 index 0000000000..15771c78bf --- /dev/null +++ b/spec/models/license_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe License do + + context "associations" do + it { is_expected.to have_many :research_outputs } + end + + context "scopes" do + describe "#selectable" do + before(:each) do + @license = create(:license, deprecated: false) + @deprecated = create(:license, deprecated: true) + end + + it "does not include deprecated licenses" do + expect(described_class.selectable.include?(@deprecated)).to eql(false) + end + it "includes non-depracated licenses" do + expect(described_class.selectable.include?(@license)).to eql(true) + end + end + + describe "#preferred" do + before(:each) do + @preferred_license = create(:license, deprecated: false) + @non_preferred_license = create(:license, deprecated: false) + + @preferred_oldest = create(:license, deprecated: false) + @preferred_older = create(:license, identifier: "#{@preferred_oldest.identifier}-1.0", + deprecated: false) + @preferred_latest = create(:license, identifier: "#{@preferred_oldest.identifier}-1.1", + deprecated: false) + + Rails.configuration.x.madmp.preferred_licenses = [ + @preferred_license.identifier, + "#{@preferred_oldest.identifier}-%{latest}" + ] + end + + it "calls :selectable if no preferences are defined in the app config" do + Rails.configuration.x.madmp.preferred_licenses = nil + described_class.expects(:selectable).returns([@license]) + described_class.preferred + end + it "does not include non-preferred licenses" do + expect(described_class.preferred.include?(@non_preferred_license)).to eql(false) + end + it "includes preferred licenses" do + expect(described_class.preferred.include?(@preferred_license)).to eql(true) + end + it "includes the latest version of a preferred licenses" do + expect(described_class.preferred.include?(@preferred_latest)).to eql(true) + expect(described_class.preferred.include?(@preferred_oldest)).to eql(false) + expect(described_class.preferred.include?(@preferred_older)).to eql(false) + end + end + end +end diff --git a/spec/models/metadata_standard_spec.rb b/spec/models/metadata_standard_spec.rb new file mode 100644 index 0000000000..a3ea22babb --- /dev/null +++ b/spec/models/metadata_standard_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe MetadataStandard do + + context "associations" do + it { is_expected.to have_and_belong_to_many :research_outputs } + end + + context "scopes" do + before(:each) do + @name_part = "Foobar" + @by_title = create(:metadata_standard, title: [Faker::Lorem.sentence, @name_part].join(" ")) + desc = [@name_part, Faker::Lorem.paragraph].join(" ") + @by_description = create(:metadata_standard, description: desc) + end + + it ":search returns the expected records" do + results = described_class.search(@name_part) + expect(results.include?(@by_title)).to eql(true) + expect(results.include?(@by_description)).to eql(true) + + results = described_class.search("Zzzzzz") + expect(results.include?(@by_title)).to eql(false) + expect(results.include?(@by_description)).to eql(false) + end + + end + +end diff --git a/spec/models/plan_spec.rb b/spec/models/plan_spec.rb index 2f8f38c2de..6f9da0dd76 100644 --- a/spec/models/plan_spec.rb +++ b/spec/models/plan_spec.rb @@ -1122,7 +1122,7 @@ describe "#latest_update" do - let!(:plan) { create(:plan, :creator, updated_at: 5.minutes.ago) } + let!(:plan) { create(:plan, updated_at: 5.minutes.ago) } subject { plan.latest_update.to_i } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb new file mode 100644 index 0000000000..6721b47b10 --- /dev/null +++ b/spec/models/repository_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: repositories +# +# id :integer not null, primary key +# name :string not null +# description :text +# url :string +# contact :string +# info :json +# created_at :datetime +# updated_at :datetime +# +# Indexes +# +# index_repositories_on_name (name) +# index_repositories_on_url (url) +# + +require "rails_helper" + +describe Repository do + + context "associations" do + it { is_expected.to have_and_belong_to_many :research_outputs } + end + + context "scopes" do + before(:each) do + @types = [Faker::Music::PearlJam.unique.song, Faker::Music::PearlJam.unique.song] + @subjects = [Faker::Music::PearlJam.unique.musician, Faker::Music::PearlJam.unique.musician] + @keywords = [Faker::Music::GratefulDead.unique.song, Faker::Music::GratefulDead.unique.song] + + @never_found = create(:repository, name: "foo", info: { types: [@types.last], + subjects: [@subjects.last], + keywords: [@keywords.last] }) + + @by_type = create(:repository, info: { types: [@types.first], + subjects: [@subjects.last], + keywords: [@keywords.last] }) + @by_subject = create(:repository, info: { types: [@types.last], + subjects: [@subjects.first], + keywords: [@keywords.last] }) + @by_facet = create(:repository, info: { types: [@types.last], + subjects: [@subjects.last], + keywords: [@keywords.first] }) + end + + describe "#by_type" do + it "returns the expected repositories" do + results = described_class.by_type(@types.first) + expect(results.include?(@never_found)).to eql(false) + expect(results.include?(@by_type)).to eql(true) + expect(results.include?(@by_subject)).to eql(false) + expect(results.include?(@by_facet)).to eql(false) + end + end + + describe "#by_subject" do + it "returns the expected repositories" do + results = described_class.by_subject(@subjects.first) + expect(results.include?(@never_found)).to eql(false) + expect(results.include?(@by_type)).to eql(false) + expect(results.include?(@by_subject)).to eql(true) + expect(results.include?(@by_facet)).to eql(false) + end + end + + describe "#by_facet" do + it "returns the expected repositories" do + results = described_class.by_facet(@keywords.first) + expect(results.include?(@never_found)).to eql(false) + expect(results.include?(@by_type)).to eql(false) + expect(results.include?(@by_subject)).to eql(false) + expect(results.include?(@by_facet)).to eql(true) + end + end + + describe "#search" do + it "returns repositories with keywords like the search term" do + results = described_class.search(@keywords.first[1..3]) + expect(results.include?(@never_found)).to eql(false) + expect(results.include?(@by_type)).to eql(false) + expect(results.include?(@by_subject)).to eql(false) + expect(results.include?(@by_facet)).to eql(true) + end + it "returns repositories with subjects like the search term" do + results = described_class.search(@by_subject.name[1..@by_subject.name.length - 1]) + expect(results.include?(@never_found)).to eql(false) + expect(results.include?(@by_type)).to eql(false) + expect(results.include?(@by_subject)).to eql(true) + end + it "returns repositories with name like the search term" do + repo = create(:repository, name: [Faker::Lorem.word, @by_subject.name].join(" ")) + results = described_class.search(@by_subject.name[1..@by_subject.name.length - 1]) + expect(results.include?(@never_found)).to eql(false) + expect(results.include?(repo)).to eql(true) + end + end + end +end diff --git a/spec/models/research_output_spec.rb b/spec/models/research_output_spec.rb index d948f6c206..799a8a36e5 100644 --- a/spec/models/research_output_spec.rb +++ b/spec/models/research_output_spec.rb @@ -5,7 +5,7 @@ RSpec.describe ResearchOutput, type: :model do context "associations" do - it { is_expected.to belong_to(:plan).optional } + it { is_expected.to belong_to(:plan).optional.touch(true) } end # rubocop:disable Layout/LineLength @@ -20,8 +20,8 @@ it { is_expected.to validate_presence_of(:access) } it { is_expected.to validate_presence_of(:title) } - it { expect(@subject).to validate_uniqueness_of(:title).scoped_to(:plan_id) } - it { expect(@subject).to validate_uniqueness_of(:abbreviation).scoped_to(:plan_id) } + it { expect(@subject).to validate_uniqueness_of(:title).case_insensitive.scoped_to(:plan_id).with_message("must be unique") } + it { expect(@subject).to validate_uniqueness_of(:abbreviation).case_insensitive.scoped_to(:plan_id).with_message("must be unique") } it "requires :output_type_description if :output_type is 'other'" do @subject.other! @@ -36,12 +36,11 @@ it "factory builds a valid model" do expect(build(:research_output).valid?).to eql(true) - expect(build(:research_output, :complete).valid?).to eql(true) end describe "cascading deletes" do it "does not delete associated plan" do - model = create(:research_output, :complete, plan: create(:plan)) + model = create(:research_output, plan: create(:plan)) plan = model.plan model.destroy expect(Plan.last).to eql(plan) diff --git a/spec/presenters/research_output_presenter_spec.rb b/spec/presenters/research_output_presenter_spec.rb new file mode 100644 index 0000000000..3bf65a1925 --- /dev/null +++ b/spec/presenters/research_output_presenter_spec.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ResearchOutputPresenter do + + before(:each) do + @research_output = create(:research_output, plan: create(:plan)) + @presenter = described_class.new(research_output: @research_output) + end + + describe ":selectable_output_types" do + it "returns the output types" do + expect(@presenter.selectable_output_types.any?).to eql(true) + end + it "packages the output types for a selectbox - [['Audiovisual', 'audiovisual']]" do + sample = @presenter.selectable_output_types.first + expect(sample.length).to eql(2) + expect(sample[0].scan(/^[a-zA-Z\s]*$/).any?).to eql(true) + expect(sample[1].scan(/^[a-z]*$/).any?).to eql(true) + expect(sample[0].underscore).to eql(sample[1]) + expect(ResearchOutput.output_types[sample[1]].present?).to eql(true) + end + end + + describe ":selectable_access_types" do + it "returns the output types" do + expect(@presenter.selectable_access_types.any?).to eql(true) + end + it "packages the output types for a selectbox - [['Open', 'open']]" do + sample = @presenter.selectable_access_types.first + expect(sample.length).to eql(2) + expect(sample[0].scan(/^[a-zA-Z\s]*$/).any?).to eql(true) + expect(sample[1].scan(/^[a-z]*$/).any?).to eql(true) + expect(sample[0].underscore).to eql(sample[1]) + expect(ResearchOutput.accesses[sample[1]].present?).to eql(true) + end + end + + describe ":selectable_size_units" do + it "returns the output types" do + expect(@presenter.selectable_size_units.any?).to eql(true) + end + it "packages the output types for a selectbox - [['MB', 'mb']]" do + sample = @presenter.selectable_size_units.first + expect(sample.length).to eql(2) + expect(sample[0].scan(/^[a-zA-Z\s]*$/).any?).to eql(true) + expect(sample[1].scan(/^[a-z]*$/).any?).to eql(true) + expect(sample[0].downcase).to eql(sample[1]) + end + end + + describe ":converted_file_size(size:)" do + it "returns an zero MB if size is not present" do + expect(@presenter.converted_file_size(size: nil)).to eql({ size: nil, unit: "mb" }) + end + it "returns an zero MB if size is not a number" do + expect(@presenter.converted_file_size(size: "foo")).to eql({ size: nil, unit: "mb" }) + end + it "returns an zero MB if size is not positive" do + expect(@presenter.converted_file_size(size: -1)).to eql({ size: nil, unit: "mb" }) + end + it "can handle bytes" do + expect(@presenter.converted_file_size(size: 100)).to eql({ size: 100, unit: "" }) + end + it "can handle megabytes" do + expect(@presenter.converted_file_size(size: 1.megabytes)).to eql({ size: 1, unit: "mb" }) + end + it "can handle gigabytes" do + expect(@presenter.converted_file_size(size: 1.gigabytes)).to eql({ size: 1, unit: "gb" }) + end + it "can handle terabytes" do + expect(@presenter.converted_file_size(size: 1.terabytes)).to eql({ size: 1, unit: "tb" }) + end + it "can handle petabytes" do + expect(@presenter.converted_file_size(size: 1.petabytes)).to eql({ size: 1, unit: "pb" }) + end + end + + describe ":display_name" do + it "returns an empty string unless if we do not have a ResearchOutput" do + presenter = described_class.new(research_output: build(:org)) + expect(presenter.display_name).to eql("") + end + it "does not trim names that are <= 50 characters" do + presenter = described_class.new(research_output: build(:research_output, title: "a" * 49)) + expect(presenter.display_name).to eql("a" * 49) + end + it "does not trims names that are > 50 characters" do + presenter = described_class.new(research_output: build(:research_output, title: "a" * 51)) + expect(presenter.display_name).to eql("#{'a' * 50} ...") + end + end + + describe ":display_type" do + it "returns an empty string unless if we do not have a ResearchOutput" do + presenter = described_class.new(research_output: build(:org)) + expect(presenter.display_type).to eql("") + end + it "returns the user's description if the output_type is other" do + research_output = build(:research_output, output_type: "other", + output_type_description: "foo") + presenter = described_class.new(research_output: research_output) + expect(presenter.display_type).to eql("foo") + end + it "returns the humanized version of the output_type" do + presenter = described_class.new(research_output: build(:research_output, output_type: :image)) + expect(presenter.display_type).to eql("Image") + end + end + + describe ":display_repository" do + before(:each) do + @research_output.repositories.clear + end + it "returns ['None specified'] if not repositories are assigned" do + presenter = described_class.new(research_output: @research_output) + expect(presenter.display_repository).to eql(["None specified"]) + end + it "returns an array of names when there is only one repository" do + repo = build(:repository) + @research_output.repositories << repo + presenter = described_class.new(research_output: @research_output) + expect(presenter.display_repository).to eql([repo.name]) + end + it "returns an array of names when there are multiple repositories" do + repos = [build(:repository), build(:repository)] + @research_output.repositories << repos + presenter = described_class.new(research_output: @research_output) + expect(presenter.display_repository).to eql(repos.collect(&:name)) + end + end + + describe ":display_access" do + it "returns 'Unspecified' if :access has not been defined" do + presenter = described_class.new(research_output: build(:research_output, access: nil)) + expect(presenter.display_access).to eql("Unspecified") + end + it "returns a humanized version of the :access enum selection" do + presenter = described_class.new(research_output: build(:research_output, access: :open)) + expect(presenter.display_access).to eql("Open") + end + end + + describe ":display_release" do + it "returns 'Unspecified' if :access has not been defined" do + presenter = described_class.new(research_output: build(:research_output, release_date: nil)) + expect(presenter.display_release).to eql("Unspecified") + end + it "returns a the release_date as a Date" do + now = Time.now + presenter = described_class.new(research_output: build(:research_output, release_date: now)) + expect(presenter.display_release.is_a?(Date)).to eql(true) + end + end + + context "class methods" do + describe ":selectable_subjects" do + it "returns subjects" do + expect(described_class.selectable_subjects.any?).to eql(true) + end + it "packages the subjects for a selectbox - [['Biology', '21 Biology']]" do + sample = described_class.selectable_subjects.first + expect(sample.length).to eql(2) + expect(sample[0].scan(/^[a-zA-Z\s,]*$/).any?).to eql(true) + expect(sample[1].scan(/^[0-9]+\s[a-zA-Z\s,]*$/).any?).to eql(true) + expect(sample[1].ends_with?(sample[0])).to eql(true) + end + end + + describe ":selectable_repository_types" do + it "returns repository types" do + expect(described_class.selectable_repository_types.any?).to eql(true) + end + it "packages the repo types for a selectbox - [['Discipline specific', 'disciplinary']]" do + sample = described_class.selectable_repository_types.first + expect(sample.length).to eql(2) + expect(sample[0].scan(/^[A-Z]{1}[a-z\s()]*$/).any?).to eql(true) + expect(sample[1].scan(/^[a-z]*$/).any?).to eql(true) + end + end + end + +end diff --git a/spec/services/external_apis/rdamsc_service_spec.rb b/spec/services/external_apis/rdamsc_service_spec.rb new file mode 100644 index 0000000000..ad430a7579 --- /dev/null +++ b/spec/services/external_apis/rdamsc_service_spec.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ExternalApis::RdamscService do + + include Webmocks + + before(:each) do + MetadataStandard.all.destroy_all + + @rdams_results = { + "apiVersion": "2.0.0", + "data": { + "currentItemCount": Faker::Number.number(digits: 2), + "items": [ + { + "description": Faker::Lorem.paragraph, + "keywords": [ + Faker::Internet.unique.url + ], + "locations": [ + { "type": %w[document website].sample, "url": Faker::Internet.unique.url } + ], + "mscid": "msc:m#{Faker::Number.number(digits: 2)}", + "relatedEntities": [ + { "id": "msc:m#{Faker::Number.number(digits: 2)}", "role": %w[scheme child].sample } + ], + "slug": SecureRandom.uuid, + "title": Faker::Lorem.sentence, + "uri": Faker::Internet.unique.url + } + ] + } + } + + stub_rdamsc_service(true, @rdams_results.to_json) + end + + describe ":fetch_metadata_standards" do + it "calls :query_schemes" do + described_class.expects(:query_schemes).returns(nil) + expect(described_class.fetch_metadata_standards).to eql(nil) + end + end + + context "private methods" do + describe ":query_api(path:)" do + it "returns nil if path is not present" do + expect(described_class.send(:query_api, path: nil)).to eql(nil) + end + it "calls the error handler if an HTTP 200 is not received from the SPDX API" do + stub_rdamsc_service(false) + described_class.expects(:handle_http_failure) + expect(described_class.send(:query_api, path: Faker::Lorem.word)).to eql(nil) + end + it "logs an error if the response was invalid JSON" do + JSON.expects(:parse).raises(JSON::ParserError.new) + described_class.expects(:log_error) + expect(described_class.send(:query_api, path: Faker::Lorem.word)).to eql(nil) + end + it "reuturns the array of response body as JSON" do + expected = JSON.parse(@rdams_results.to_json) + expect(described_class.send(:query_api, path: Faker::Lorem.word)).to eql(expected) + end + end + + describe ":query_schemes(path:)" do + before(:each) do + @path = Faker::Internet.unique.url + end + it "returns false if the initial query returned no results" do + described_class.expects(:query_api).with(path: @path).returns(nil) + expect(described_class.send(:query_schemes, path: @path)).to eql(false) + end + it "calls :process_scheme_entries if the query returned results" do + described_class.expects(:query_api).with(path: @path).returns(@rdams_results) + described_class.expects(:process_scheme_entries) + described_class.send(:query_schemes, path: @path) + end + it "recursively calls itself while a 'nextLink' is provided in the query results" do + hash = @rdams_results + hash[:data][:nextLink] = "#{@path}/next" + described_class.expects(:query_api) + .with(path: @path).returns(hash.with_indifferent_access) + described_class.expects(:query_api) + .with(path: hash[:data][:nextLink]).returns(@rdams_results) + described_class.expects(:process_scheme_entries).twice + described_class.send(:query_schemes, path: @path) + end + end + + describe ":process_scheme_entries(json:)" do + it "returns false if json is not present" do + expect(described_class.send(:process_scheme_entries, json: nil)).to eql(false) + end + it "returns false if json does not contain :data not present" do + expect(described_class.send(:process_scheme_entries, json: { "foo": "bar" })).to eql(false) + end + it "returns false if json[:data] does not contain :items present" do + json = { "data": { "items": [] } } + expect(described_class.send(:process_scheme_entries, json: json)).to eql(false) + end + it "updates the MetadataStandard if it already exists" do + hash = @rdams_results[:data][:items].first + standard = create(:metadata_standard, uri: hash[:uri], + title: hash[:title]) + + expect(described_class.send(:process_scheme_entries, + json: JSON.parse(@rdams_results.to_json))) + result = MetadataStandard.last + expect(result.id).to eql(standard.id) + expect(result.title).to eql(hash[:title]) + expect(result.uri).to eql(hash[:uri]) + + expect(result.description).to eql(hash[:description]) + expect(result.rdamsc_id).to eql(hash[:mscid]) + expect(result.locations).to eql(JSON.parse(hash[:locations].to_json)) + expect(result.related_entities).to eql(JSON.parse(hash[:relatedEntities].to_json)) + end + it "creates a new MetadataStandard" do + hash = @rdams_results[:data][:items].first + + expect(described_class.send(:process_scheme_entries, + json: JSON.parse(@rdams_results.to_json))) + result = MetadataStandard.last + expect(result.title).to eql(hash[:title]) + expect(result.description).to eql(hash[:description]) + expect(result.rdamsc_id).to eql(hash[:mscid]) + expect(result.uri).to eql(hash[:uri]) + expect(result.locations).to eql(JSON.parse(hash[:locations].to_json)) + expect(result.related_entities).to eql(JSON.parse(hash[:relatedEntities].to_json)) + end + end + end + +end diff --git a/spec/services/external_apis/re3data_service_spec.rb b/spec/services/external_apis/re3data_service_spec.rb new file mode 100644 index 0000000000..eced9b4048 --- /dev/null +++ b/spec/services/external_apis/re3data_service_spec.rb @@ -0,0 +1,320 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ExternalApis::Re3dataService do + + before(:each) do + @repo_id = "r3d#{Faker::Number.number(digits: 6)}" + @headers = described_class.headers + @repositories_path = "#{described_class.api_base_url}#{described_class.list_path}" + path = "#{described_class.api_base_url}#{described_class.repository_path}#{@repo_id}" + @repository_path = URI(path) + + @repositories_results = <<-XML + + + + #{@repo_id} + #{Faker::Company.name} + + + + XML + @repository_result = <<-XML + + + + + #{@repo_id} + #{Faker::Lorem.word.upcase} + #{Faker::Internet.url} + #{Faker::Lorem.word}:#{Faker::Number.number(digits: 5)} + #{Faker::Lorem.sentence} + #{Faker::Internet.email} + #{%w[disciplinary institutional other].sample} + #{Faker::Number.number(digits: 4)} data packages + 2021 + + eng + 1 Humanities and Social Sciences + #{Faker::Internet.url} + Plain text + dataProvider + #{Faker::Lorem.word} + + #{Faker::Company.name} + #{Faker::Lorem.word.upcase} + USA + general + non-profit + #{Faker::Internet.url} + 2021 + + + + + #{Faker::Lorem.sentence} + #{Faker::Internet.url} + + #{Faker::Lorem.word} + + #{Faker::Lorem.word} + #{Faker::Internet.url} + + #{Faker::Lorem.word} + + #{Faker::Lorem.word} + #{Faker::Internet.url} + + + #{Faker::Lorem.word} + #{Faker::Lorem.word} + + + #{Faker::Lorem.word} + #{Faker::Internet.url} + + #{Faker::Lorem.word} + #{%w[no yes].sample} + #{Faker::Internet.url} + #{%w[ARK DOI handle].sample} + #{Faker::Internet.url} + #{Faker::Lorem.word.upcase} + #{%w[no yes].sample} + #{%w[no yes].sample} + + #{Faker::Lorem.sentence} + #{Faker::Internet.url} + + #{Faker::Lorem.sentence} + 2021-02-03 + 2021-02-03 + + + XML + + end + + describe "#fetch" do + context "#fetch" do + it "returns an empty array if re3data did not return a repository list" do + described_class.expects(:query_re3data).returns(nil) + expect(described_class.fetch).to eql([]) + end + it "fetches individual repository data" do + described_class.expects(:query_re3data) + .returns(Nokogiri::XML(@repositories_results, nil, "utf8")) + described_class.expects(:query_re3data_repository).at_least(1) + described_class.fetch + end + it "processes the repository data" do + described_class.expects(:query_re3data) + .returns(Nokogiri::XML(@repositories_results, nil, "utf8")) + described_class.expects(:query_re3data_repository) + .returns(Nokogiri::XML(@repository_result, nil, "utf8")) + described_class.expects(:process_repository).at_least(1) + described_class.fetch + end + end + end + + context "private methods" do + describe "#query_re3data" do + it "calls the handle_http_failure method if a non 200 response is received" do + stub_request(:get, @repositories_path).with(headers: @headers) + .to_return(status: 403, body: "", headers: {}) + described_class.expects(:handle_http_failure).at_least(1) + expect(described_class.send(:query_re3data)).to eql(nil) + end + it "returns the response body as XML" do + stub_request(:get, @repositories_path).with(headers: @headers) + .to_return( + status: 200, + body: @repositories_results, + headers: {} + ) + expected = Nokogiri::XML(@repositories_results, nil, "utf8").text + expect(described_class.send(:query_re3data).text).to eql(expected) + end + end + + describe "#query_re3data_repository(repo_id:)" do + it "returns an empty array if term is blank" do + expect(described_class.send(:query_re3data_repository, repo_id: nil)).to eql([]) + end + it "calls the handle_http_failure method if a non 200 response is received" do + stub_request(:get, @repository_path).with(headers: @headers) + .to_return(status: 403, body: "", headers: {}) + described_class.expects(:handle_http_failure).at_least(1) + expect(described_class.send(:query_re3data_repository, repo_id: @repo_id)).to eql([]) + end + it "returns the response body as JSON" do + stub_request(:get, @repository_path).with(headers: @headers) + .to_return( + status: 200, + body: @repository_result, + headers: {} + ) + expected = Nokogiri::XML(@repository_result, nil, "utf8").text + result = described_class.send(:query_re3data_repository, repo_id: @repo_id).text + expect(result).to eql(expected) + end + end + + describe "#process_repository(id:, node:)" do + before(:each) do + @node = Nokogiri::XML(@repository_result, nil, "utf8") + @repo = @node.xpath("//r3d:re3data//r3d:repository").first + end + it "returns nil if :id is not present" do + expect(described_class.send(:process_repository, id: nil, node: @repo)).to eql(nil) + end + it "returns nil if :node is not present" do + expect(described_class.send(:process_repository, id: @repo_id, node: nil)).to eql(nil) + end + it "finds an existing Repository by its identifier" do + repo = create(:repository, uri: @repo_id) + expect(described_class.send(:process_repository, id: @repo_id, node: @repo)).to eql(repo) + end + it "finds an existing Repository by its homepage" do + repo = create(:repository, homepage: @repo.xpath("//r3d:repositoryURL")&.text) + expect(described_class.send(:process_repository, id: @repo_id, node: @repo)).to eql(repo) + end + it "creates a new Repository" do + repo = described_class.send(:process_repository, id: @repo_id, node: @repo) + expect(repo.new_record?).to eql(false) + expect(repo.name).to eql(@repo.xpath("//r3d:repositoryName")&.text) + end + it "attaches the identifier to the Repository (if it is not already defined" do + repo = described_class.send(:process_repository, id: @repo_id, node: @repo) + expect(repo.uri.ends_with?(@repo_id)).to eql(true) + end + end + + describe "#parse_repository(repo:, node:)" do + before(:each) do + doc = Nokogiri::XML(@repository_result, nil, "utf8") + @node = doc.xpath("//r3d:re3data//r3d:repository").first + @repo = create(:repository, name: @node.xpath("//r3d:repositoryName")&.text) + end + it "returns nil if :repo is not present" do + expect(described_class.send(:parse_repository, repo: nil, node: @node)).to eql(nil) + end + it "returns nil if :node is not present" do + expect(described_class.send(:parse_repository, repo: @repo, node: nil)).to eql(nil) + end + it "updates the :description" do + described_class.send(:parse_repository, repo: @repo, node: @node) + expect(@repo.description).to eql(@node.xpath("//r3d:description")&.text) + end + it "updates the :homepage" do + described_class.send(:parse_repository, repo: @repo, node: @node) + expect(@repo.homepage).to eql(@node.xpath("//r3d:repositoryURL")&.text) + end + it "updates the :contact" do + described_class.send(:parse_repository, repo: @repo, node: @node) + expect(@repo.contact).to eql(@node.xpath("//r3d:repositoryContact")&.text) + end + it "updates the :info" do + described_class.send(:parse_repository, repo: @repo, node: @node) + expect(@repo.info.present?).to eql(true) + end + context ":info JSON content" do + before(:each) do + policies = @node.xpath("//r3d:policy").map do |node| + described_class.send(:parse_policy, node: node) + end + upload_types = @node.xpath("//r3d:dataUpload").map do |node| + described_class.send(:parse_upload, node: node) + end + + @expected = { + 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: policies, + upload_types: upload_types + } + end + it "updates the :types" do + described_class.send(:parse_repository, repo: @repo, node: @node) + expect(@repo.info["types"]).to eql(@expected[:types]) + end + it "updates the :subjects" do + described_class.send(:parse_repository, repo: @repo, node: @node) + expect(@repo.info["subjects"]).to eql(@expected[:subjects]) + end + it "updates the :provider_types" do + described_class.send(:parse_repository, repo: @repo, node: @node) + expect(@repo.info["provider_types"]).to eql(@expected[:provider_types]) + end + it "updates the :keywords" do + described_class.send(:parse_repository, repo: @repo, node: @node) + expect(@repo.info["keywords"]).to eql(@expected[:keywords]) + end + it "updates the :access" do + described_class.send(:parse_repository, repo: @repo, node: @node) + expect(@repo.info["access"]).to eql(@expected[:access]) + end + it "updates the :pid_system" do + described_class.send(:parse_repository, repo: @repo, node: @node) + expect(@repo.info["pid_system"]).to eql(@expected[:pid_system]) + end + it "updates the :policies" do + described_class.send(:parse_repository, repo: @repo, node: @node) + expect(@repo.info["policies"].to_json).to eql(@expected[:policies].to_json) + end + it "updates the :upload_types" do + described_class.send(:parse_repository, repo: @repo, node: @node) + expect(@repo.info["upload_types"].to_json).to eql(@expected[:upload_types].to_json) + end + end + end + + describe "#parse_policy(node:)" do + before(:each) do + @node = Nokogiri::XML(@repository_result, nil, "utf8") + base = @node.xpath("//r3d:re3data//r3d:repository").first + @expected = { + name: base.xpath("r3d:policyName")&.text, + url: base.xpath("r3d:policyURL")&.text + } + end + it "returns nil if :node is not present" do + expect(described_class.send(:parse_policy, node: nil)).to eql(nil) + end + it "updates the :name" do + expect(described_class.send(:parse_policy, node: @node)[:name]).to eql(@expected[:name]) + end + it "updates the :url" do + expect(described_class.send(:parse_policy, node: @node)[:url]).to eql(@expected[:url]) + end + end + + describe "#parse_upload(node:)" do + before(:each) do + @node = Nokogiri::XML(@repository_result, nil, "utf8") + base = @node.xpath("//r3d:re3data//r3d:repository").first + @expected = { + type: base.xpath("r3d:dataUploadType")&.text, + restriction: base.xpath("r3d:dataUploadRestriction")&.text + } + end + it "returns nil if :node is not present" do + expect(described_class.send(:parse_upload, node: nil)).to eql(nil) + end + it "updates the :type" do + expect(described_class.send(:parse_upload, node: @node)[:type]).to eql(@expected[:type]) + end + it "updates the :restriction" do + result = described_class.send(:parse_upload, node: @node)[:restriction] + expect(result).to eql(@expected[:restriction]) + end + end + + end +end diff --git a/spec/services/external_apis/spdx_service_spec.rb b/spec/services/external_apis/spdx_service_spec.rb new file mode 100644 index 0000000000..16682f281a --- /dev/null +++ b/spec/services/external_apis/spdx_service_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ExternalApis::SpdxService do + + include Webmocks + + before(:each) do + License.all.destroy_all + + @licenses_results = { + "reference": "./#{Faker::Lorem.unique.word}.html", + "isDeprecatedLicenseId": [true, false].sample, + "detailsUrl": Faker::Internet.unique.url, + "referenceNumber": Faker::Number.unique.number(digits: 2), + "name": Faker::Music::PearlJam.unique.album, + "licenseId": Faker::Music::PearlJam.unique.song.upcase.gsub(/\s/, "_"), + "seeAlso": [ + Faker::Internet.unique.url + ], + "isOsiApproved": [true, false].sample + } + + stub_spdx_service(true, { "licenses": @licenses_results }.to_json) + end + + describe ":fetch" do + it "returns an empty array if spdx did not return a repository list" do + described_class.expects(:query_spdx).returns(nil) + expect(described_class.fetch).to eql([]) + end + it "fetches the licenses" do + described_class.expects(:query_spdx).returns({ "licenses": @licenses_results }) + described_class.expects(:process_license).returns(true) + described_class.fetch + end + end + + context "private methods" do + describe ":query_spdx" do + it "calls the error handler if an HTTP 200 is not received from the SPDX API" do + stub_spdx_service(false) + described_class.expects(:handle_http_failure) + expect(described_class.send(:query_spdx)).to eql([]) + end + it "logs an error if the response was invalid JSON" do + JSON.expects(:parse).raises(JSON::ParserError.new) + described_class.expects(:log_error) + expect(described_class.send(:query_spdx)).to eql([]) + end + it "returns an empty array if the response conatins no license" do + JSON.expects(:parse).returns({}) + expect(described_class.send(:query_spdx)).to eql([]) + end + it "reuturns the array of licenses" do + expect(described_class.send(:query_spdx)).to eql(JSON.parse(@licenses_results.to_json)) + end + end + + describe ":process_license(hash:)" do + it "returns nil if hash is empty" do + expect(described_class.send(:process_license, hash: nil)).to eql(nil) + end + + it "returns nil if a License could not be initialized" do + License.expects(:find_or_initialize_by).returns(nil) + expect(described_class.send(:process_license, hash: @licenses_results)).to eql(nil) + end + + it "updates existing License records" do + hash = @licenses_results + license = create(:license, identifier: hash[:licenseId]) + + expect(described_class.send(:process_license, hash: JSON.parse(hash.to_json))) + result = License.last + expect(result.id).to eql(license.id) + expect(result.name).to eql(hash[:name]) + expect(result.uri).to eql(hash[:detailsUrl]) + expect(result.osi_approved).to eql(hash[:isOsiApproved]) + expect(result.deprecated).to eql(hash[:isDeprecatedLicenseId]) + end + + it "creates new License records" do + hash = @licenses_results + + expect(described_class.send(:process_license, hash: JSON.parse(hash.to_json))) + result = License.last + expect(result.identifier).to eql(hash[:licenseId]) + expect(result.name).to eql(hash[:name]) + expect(result.uri).to eql(hash[:detailsUrl]) + expect(result.osi_approved).to eql(hash[:isOsiApproved]) + expect(result.deprecated).to eql(hash[:isDeprecatedLicenseId]) + end + end + end + +end diff --git a/spec/support/helpers/webmocks.rb b/spec/support/helpers/webmocks.rb index b5413f2b48..2acd5f8af2 100644 --- a/spec/support/helpers/webmocks.rb +++ b/spec/support/helpers/webmocks.rb @@ -17,6 +17,16 @@ def stub_ror_service .to_return(status: 200, body: mocked_ror_response, headers: {}) end + def stub_spdx_service(successful = true, response_body = "") + stub_request(:get, %r{https://raw.githubusercontent.com/spdx/.*}) + .to_return(status: successful ? 200 : 500, body: response_body, headers: {}) + end + + def stub_rdamsc_service(successful = true, response_body = "") + stub_request(:get, %r{https://rdamsc.bath.ac.uk/.*}) + .to_return(status: successful ? 200 : 500, body: response_body, headers: {}) + end + def stub_openaire url = ExternalApis::OpenAireService.api_base_url url = "#{url}#{ExternalApis::OpenAireService.search_path}" diff --git a/spec/views/api/v1/datasets/_show.json.jbuilder_spec.rb b/spec/views/api/v1/datasets/_show.json.jbuilder_spec.rb index 5f9bd63a40..de1a1b502e 100644 --- a/spec/views/api/v1/datasets/_show.json.jbuilder_spec.rb +++ b/spec/views/api/v1/datasets/_show.json.jbuilder_spec.rb @@ -4,27 +4,125 @@ describe "api/v1/datasets/_show.json.jbuilder" do - before(:each) do - # TODO: Implement this once the Dataset models are in place - @plan = create(:plan) - render partial: "api/v1/datasets/show", locals: { plan: @plan } - @json = JSON.parse(rendered).with_indifferent_access + context "config has disabled madmp options" do + before(:each) do + @plan = create(:plan) + @output = create(:research_output, plan: @plan) + end + + it "does not include :preservation_statement if config is false" do + Rails.configuration.x.madmp.extract_preservation_statements_from_themed_questions = false + render partial: "api/v1/datasets/show", locals: { output: @output } + json = JSON.parse(rendered).with_indifferent_access + expect(json[:preservation_statement]).to eql("") + end + it "does not include :security_and_privacy if config is false" do + Rails.configuration.x.madmp.extract_security_privacy_statements_from_themed_questions = false + render partial: "api/v1/datasets/show", locals: { output: @output } + json = JSON.parse(rendered).with_indifferent_access + expect(json[:security_and_privacy]).to eql([]) + end + it "does not include :data_quality_assurance if config is false" do + Rails.configuration.x.madmp.extract_data_quality_statements_from_themed_questions = false + render partial: "api/v1/datasets/show", locals: { output: @output } + json = JSON.parse(rendered).with_indifferent_access + expect(json[:data_quality_assurance]).to eql("") + end end - describe "includes all of the dataset attributes" do - it "includes :title" do - expect(@json[:title]).to eql("Generic Dataset") + context "config has enabled madmp options" do + before(:each) do + Rails.configuration.x.madmp.extract_preservation_statements_from_themed_questions = true + Rails.configuration.x.madmp.extract_security_privacy_statements_from_themed_questions = true + Rails.configuration.x.madmp.extract_data_quality_statements_from_themed_questions = true + + @plan = create(:plan) + @output = create(:research_output, plan: @plan) + render partial: "api/v1/datasets/show", locals: { output: @output } + @json = JSON.parse(rendered).with_indifferent_access end - it "includes :personal_data" do - expect(@json[:personal_data]).to eql("unknown") + + describe "includes all of the dataset attributes" do + it "includes :type" do + expect(@json[:type]).to eql(@output.output_type) + end + it "includes :title" do + expect(@json[:title]).to eql(@output.title) + end + it "includes :description" do + expect(@json[:description]).to eql(@output.description) + end + it "includes :personal_data" do + expected = Api::V1::ApiPresenter.boolean_to_yes_no_unknown(value: @output.personal_data) + expect(@json[:personal_data]).to eql(expected) + end + it "includes :sensitive_data" do + expected = Api::V1::ApiPresenter.boolean_to_yes_no_unknown(value: @output.sensitive_data) + expect(@json[:sensitive_data]).to eql(expected) + end + it "includes :issued" do + expect(@json[:issued]).to eql(@output.release_date&.to_formatted_s(:iso8601)) + end + it "includes :dataset_id" do + expect(@json[:dataset_id][:type]).to eql("other") + expect(@json[:dataset_id][:identifier]).to eql(@output.id.to_s) + end + context ":distribution info" do + before(:each) do + @distribution = @json[:distribution].first + end + it "includes :byte_size" do + expect(@distribution[:byte_size]).to eql(@output.byte_size) + end + it "includes :data_access" do + expect(@distribution[:data_access]).to eql(@output.access) + end + it "includes :format" do + expect(@distribution[:format]).to eql(nil) + end + end + it "includes :metadata" do + expect(@json[:metadata]).not_to eql([]) + expect(@json[:metadata].first[:description].present?).to eql(true) + expect(@json[:metadata].first[:metadata_standard_id].present?).to eql(true) + expect(@json[:metadata].first[:metadata_standard_id][:type].present?).to eql(true) + expect(@json[:metadata].first[:metadata_standard_id][:identifier].present?).to eql(true) + end + it "includes :technical_resources" do + expect(@json[:technical_resources]).to eql(nil) + end end - it "includes :sensitive_data" do - expect(@json[:sensitive_data]).to eql("unknown") + + describe "includes all of the repository info as attributes" do + before(:each) do + @host = @json[:distribution].first[:host] + @expected = @output.repositories.last + end + it "includes :title" do + expect(@host[:title]).to eql(@expected.name) + end + it "includes :description" do + expect(@host[:description]).to eql(@expected.description) + end + it "includes :url" do + expect(@host[:url]).to eql(@expected.homepage) + end + it "includes :dmproadmap_host_id" do + expect(@host[:dmproadmap_host_id][:type]).to eql("url") + expect(@host[:dmproadmap_host_id][:identifier]).to eql(@expected.uri) + end end - it "includes :dataset_id" do - expect(@json[:dataset_id][:type]).to eql("url") - url = Rails.application.routes.url_helpers.api_v1_plan_url(@plan) - expect(@json[:dataset_id][:identifier]).to eql(url) + + describe "includes all of the themed question/answers as attributes" do + it "includes :preservation_statement" do + expect(@json[:preservation_statement]).to eql("") + end + it "includes :security_and_privacy" do + expect(@json[:security_and_privacy]).to eql([]) + end + it "includes :data_quality_assurance" do + expect(@json[:data_quality_assurance]).to eql("") + end end end diff --git a/spec/views/layouts/modal_search/_form.html.erb_spec.rb b/spec/views/layouts/modal_search/_form.html.erb_spec.rb new file mode 100644 index 0000000000..1fef4bd7ee --- /dev/null +++ b/spec/views/layouts/modal_search/_form.html.erb_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "layouts/modal_search/_form.html.erb" do + + before(:each) do + @model = create(:plan) + end + + it "defaults to :search_examples to an empty string and :results to an empty array" do + render partial: "layouts/modal_search/form", + locals: { + namespace: nil, + label: nil, + search_examples: nil, + model_instance: @model, + search_path: nil, + search_method: nil + } + expect(rendered.include?("- Enter a search term -")).to eql(true) + expect(rendered.include?("No results matched your filter criteria.")).to eql(true) + end + + it "uses the specified :search_examples" do + examples = Faker::Lorem.sentence + render partial: "layouts/modal_search/form", + locals: { + namespace: nil, + label: nil, + search_examples: examples, + model_instance: @model, + search_path: nil, + search_method: nil + } + expect(rendered.include?(examples)).to eql(true) + end + + it "uses the :namespace when defining the modal search sections" do + namespace = Faker::Lorem.word.downcase + render partial: "layouts/modal_search/form", + locals: { + namespace: namespace, + label: nil, + search_examples: nil, + model_instance: @model, + search_path: nil, + search_method: nil + } + expect(rendered.include?("modal-search-#{namespace}")).to eql(true) + expect(rendered.include?("modal-search-#{namespace}-filters")).to eql(true) + expect(rendered.include?("modal-search-#{namespace}-results")).to eql(true) + end + + it "Uses the :label for the button" do + label = Faker::Lorem.word + render partial: "layouts/modal_search/form", + locals: { + namespace: nil, + label: label, + search_examples: nil, + model_instance: @model, + search_path: nil, + search_method: nil + } + expect(rendered.include?("#{label} search")).to eql(true) + end + + it "Uses the :model_instance when adding the form element" do + render partial: "layouts/modal_search/form", + locals: { + namespace: nil, + label: nil, + search_examples: nil, + model_instance: @model, + search_path: nil, + search_method: nil + } + expect(rendered.include?(plan_path(@model))).to eql(true) + end + + it "Uses the :search_path when adding the form element" do + url = Faker::Internet.url + render partial: "layouts/modal_search/form", + locals: { + namespace: nil, + label: nil, + search_examples: nil, + model_instance: @model, + search_path: url, + search_method: nil + } + expect(rendered.include?(url)).to eql(true) + end + + it "Uses the :search_method when adding the form element" do + method = %i[get put post patch delete].sample + render partial: "layouts/modal_search/form", + locals: { + namespace: nil, + label: nil, + search_examples: nil, + model_instance: @model, + search_path: nil, + search_method: method + } + expect(rendered.include?(method.to_s)).to eql(true) + end +end diff --git a/spec/views/layouts/modal_search/_result.html.erb_spec.rb b/spec/views/layouts/modal_search/_result.html.erb_spec.rb new file mode 100644 index 0000000000..92ee4b953d --- /dev/null +++ b/spec/views/layouts/modal_search/_result.html.erb_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "layouts/modal_search/_result.html.erb" do + + before(:each) do + @result = build(:repository) + end + + it "renders the :result_partial if specified" do + render partial: "layouts/modal_search/result", + locals: { + item_name_attr: :name, + result: @result, + selected: nil, + result_partial: "layouts/footer", + search_path: nil, + search_method: nil + } + expect(response).to render_template(partial: "layouts/_footer") + end + + it "does not render the :result_partial if none is specified" do + render partial: "layouts/modal_search/result", + locals: { + item_name_attr: :name, + result: @result, + selected: nil, + result_partial: nil, + search_path: nil, + search_method: nil + } + expect(response).not_to render_template(partial: "layouts/footer") + end + + it "displays the result's :item_name_attr" do + render partial: "layouts/modal_search/result", + locals: { + item_name_attr: :name, + result: @result, + selected: true, + result_partial: nil, + search_path: nil, + search_method: nil + } + expect(rendered.include?("modal-search-result-selector hidden")).to eql(true) + expect(rendered.include?("modal-search-result-unselector hidden")).to eql(false) + end + + it "hides the 'Select' button and shows the 'Remove' button when :selected is true" do + render partial: "layouts/modal_search/result", + locals: { + item_name_attr: :name, + result: @result, + selected: true, + result_partial: nil, + search_path: nil, + search_method: nil + } + expect(rendered.include?("modal-search-result-selector hidden")).to eql(true) + expect(rendered.include?("modal-search-result-unselector hidden")).to eql(false) + end + + it "shows the 'Select' button and hides the 'Remove' button when :selected is false" do + render partial: "layouts/modal_search/result", + locals: { + item_name_attr: :name, + result: @result, + selected: false, + result_partial: nil, + search_path: nil, + search_method: nil + } + expect(rendered.include?("modal-search-result-selector hidden")).to eql(false) + expect(rendered.include?("modal-search-result-unselector hidden")).to eql(true) + end + +end diff --git a/spec/views/layouts/modal_search/_results.html.erb_spec.rb b/spec/views/layouts/modal_search/_results.html.erb_spec.rb new file mode 100644 index 0000000000..1a5bffccd8 --- /dev/null +++ b/spec/views/layouts/modal_search/_results.html.erb_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "layouts/modal_search/_selections.html.erb" do + + before(:each) do + create(:repository) + @msg = "No results matched your filter criteria." + end + + it "defaults :results to an empty array, :selected to false, and has a default :no_results_msg" do + render partial: "layouts/modal_search/results", + locals: { + namespace: nil, + item_name_attr: nil, + result_partial: nil, + search_path: nil, + search_method: nil + } + expect(rendered.include?(@msg)).to eql(true) + end + + context "when :selected is false" do + it "displays pagination when :results is not empty and does not display no results message" do + render partial: "layouts/modal_search/results", + locals: { + namespace: nil, + results: Repository.all.page(1), + selected: false, + item_name_attr: nil, + result_partial: nil, + search_path: nil, + search_method: nil + } + expect(rendered.include?("modal-search-results-pagination")).to eql(true) + expect(rendered.include?(@msg)).to eql(false) + end + it "does not display pagination when :results is empty and displays the message" do + render partial: "layouts/modal_search/results", + locals: { + namespace: nil, + results: [], + selected: false, + item_name_attr: nil, + result_partial: nil, + search_path: nil, + search_method: nil + } + expect(rendered.include?("modal-search-results-pagination")).to eql(false) + expect(rendered.include?(@msg)).to eql(true) + end + end + + context "when :selected is true" do + it "does not display pagination when :results is not empty and does not display message" do + render partial: "layouts/modal_search/results", + locals: { + namespace: nil, + results: Repository.all.page(1), + selected: true, + item_name_attr: nil, + result_partial: nil, + search_path: nil, + search_method: nil + } + expect(rendered.include?("modal-search-results-pagination")).to eql(false) + expect(rendered.include?(@msg)).to eql(false) + end + it "does not display pagination when :results is empty and does not display message" do + render partial: "layouts/modal_search/results", + locals: { + namespace: nil, + results: [], + selected: true, + item_name_attr: nil, + result_partial: nil, + search_path: nil, + search_method: nil + } + expect(rendered.include?("modal-search-results-pagination")).to eql(false) + expect(rendered.include?(@msg)).to eql(false) + end + end + +end diff --git a/spec/views/layouts/modal_search/_selections.html.erb_spec.rb b/spec/views/layouts/modal_search/_selections.html.erb_spec.rb new file mode 100644 index 0000000000..05f4299b68 --- /dev/null +++ b/spec/views/layouts/modal_search/_selections.html.erb_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "layouts/modal_search/_selections.html.erb" do + + before(:each) do + @namespace = Faker::Lorem.word.downcase + @label = Faker::Lorem.sentence + render partial: "layouts/modal_search/selections", + locals: { + namespace: @namespace, + button_label: @label, + results: [], + item_name_attr: Faker::Lorem.word, + result_partial: nil, + search_path: nil, + search_method: nil + } + end + + it "adds the :namespace to the selections block" do + expect(rendered.include?("modal-search-#{@namespace}-selections")).to eql(true) + end + + it "adds the :namespace to the button" do + expect(rendered.include?("target=\"#modal-search-#{@namespace}\"")).to eql(true) + end + + it "sets the :button_label on the button" do + expect(rendered.include?(@label)).to eql(true) + end + + it "adds the renders the results partial" do + expect(response).to render_template(partial: "layouts/modal_search/_results") + end + +end From 548dbee271b2654397b99f4862012db95e59a462 Mon Sep 17 00:00:00 2001 From: briri Date: Fri, 6 Aug 2021 13:57:45 -0700 Subject: [PATCH 02/59] added feature test for the new modal search dialog --- spec/features/modal_search_spec.rb | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 spec/features/modal_search_spec.rb diff --git a/spec/features/modal_search_spec.rb b/spec/features/modal_search_spec.rb new file mode 100644 index 0000000000..e2b2163520 --- /dev/null +++ b/spec/features/modal_search_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.feature "ModalSearchDialog", type: :feature do + + include Webmocks + + before(:each) do + stub_openaire + + @model = create(:repository) + @template = create(:template) + @plan = create(:plan, :creator, template: @template) + @user = @plan.owner + sign_in_as_user(@user) + + click_link @plan.title + click_link "Research Outputs" + click_link "Add a research output" + end + + it "Modal search opens and closes and allows user to search, select and remove items", :js do + # Open the modal + click_button "Add a repository" + expect(page).to have_text("Repository search") + + within("#modal-search-repositories") do + # Search for the Repository + fill_in "research_output_search_term", with: @model.name + click_button "Apply filter(s)" + expect(page).to have_text(@model.description) + + # Select the repository and make sure it no longer appears in the search results + click_link "Select" + expect(page).not_to have_text(@model.description) + + # Close the modal + click_button "Close" + end + + # Verify that the selection was added to the main page's dom + expect(page).not_to have_text("Repository search") + expect(page).to have_text(@model.description) + # Verify that we can remove the selection + click_link "Remove" + expect(page).not_to have_text(@model.description) + end + +end From d584f4ff70caf5080fe0e2cd19154862341f721b Mon Sep 17 00:00:00 2001 From: briri Date: Mon, 9 Aug 2021 07:59:48 -0700 Subject: [PATCH 03/59] fixed issues with rubocop and tests --- spec/features/modal_search_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/modal_search_spec.rb b/spec/features/modal_search_spec.rb index e2b2163520..1a66ba46b0 100644 --- a/spec/features/modal_search_spec.rb +++ b/spec/features/modal_search_spec.rb @@ -12,7 +12,7 @@ @model = create(:repository) @template = create(:template) @plan = create(:plan, :creator, template: @template) - @user = @plan.owner + @user = @plan.owner sign_in_as_user(@user) click_link @plan.title From aa7ca9479bc280bcb054662204295b8916b42ef1 Mon Sep 17 00:00:00 2001 From: briri Date: Mon, 9 Aug 2021 09:00:31 -0700 Subject: [PATCH 04/59] fix for failing repository model test --- spec/features/modal_search_spec.rb | 3 +++ spec/models/repository_spec.rb | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/spec/features/modal_search_spec.rb b/spec/features/modal_search_spec.rb index 1a66ba46b0..8431abbdca 100644 --- a/spec/features/modal_search_spec.rb +++ b/spec/features/modal_search_spec.rb @@ -15,6 +15,9 @@ @user = @plan.owner sign_in_as_user(@user) + Rails.configuration.x.madmp.enable_research_outputs = true + Rails.configuration.x.madmp.enable_repository_selection = true + click_link @plan.title click_link "Research Outputs" click_link "Add a research output" diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 6721b47b10..b28bf86108 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -29,9 +29,9 @@ context "scopes" do before(:each) do - @types = [Faker::Music::PearlJam.unique.song, Faker::Music::PearlJam.unique.song] - @subjects = [Faker::Music::PearlJam.unique.musician, Faker::Music::PearlJam.unique.musician] - @keywords = [Faker::Music::GratefulDead.unique.song, Faker::Music::GratefulDead.unique.song] + @types = ["Armadillo", "Barracuda"] + @subjects = ["Capybara", "Dingo"] + @keywords = ["Elephant", "Falcon"] @never_found = create(:repository, name: "foo", info: { types: [@types.last], subjects: [@subjects.last], From bb1f334d1a363b9f1aad01d8bdad921bed187bc5 Mon Sep 17 00:00:00 2001 From: briri Date: Mon, 9 Aug 2021 10:17:36 -0700 Subject: [PATCH 05/59] made rubocop happy and fixed overwritten funding presenter spec --- spec/models/repository_spec.rb | 6 +-- .../api/v1/funding_presenter_spec.rb | 47 +++++++++---------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index b28bf86108..278d6f7861 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -29,9 +29,9 @@ context "scopes" do before(:each) do - @types = ["Armadillo", "Barracuda"] - @subjects = ["Capybara", "Dingo"] - @keywords = ["Elephant", "Falcon"] + @types = %w[Armadillo Barracuda] + @subjects = %w[Capybara Dingo] + @keywords = %w[Elephant Falcon] @never_found = create(:repository, name: "foo", info: { types: [@types.last], subjects: [@subjects.last], diff --git a/spec/presenters/api/v1/funding_presenter_spec.rb b/spec/presenters/api/v1/funding_presenter_spec.rb index 2216ace28c..5005df7a20 100644 --- a/spec/presenters/api/v1/funding_presenter_spec.rb +++ b/spec/presenters/api/v1/funding_presenter_spec.rb @@ -1,32 +1,29 @@ # frozen_string_literal: true -module Api +require "rails_helper" - module V1 - - class FundingPresenter - - class << self - - # If the plan has a grant number then it has been awarded/granted - # otherwise it is 'planned' - def status(plan:) - return "planned" unless plan.present? - - case plan.funding_status - when "funded" - "granted" - when "denied" - "rejected" - else - "planned" - end - end - - end +RSpec.describe Api::V1::FundingPresenter do + describe "#status(plan:)" do + it "returns `planned` if the plan is nil" do + expect(described_class.status(plan: nil)).to eql("planned") + end + it "returns `planned` if the :funding_status is nil" do + plan = build(:plan, funding_status: nil) + expect(described_class.status(plan: plan)).to eql("planned") + end + it "returns `granted` if the :funding_status is 'funded'" do + plan = build(:plan, funding_status: 'funded') + expect(described_class.status(plan: plan)).to eql("granted") + end + it "returns `rejected` if the :funding_status is 'denied'" do + plan = build(:plan, funding_status: 'denied') + expect(described_class.status(plan: plan)).to eql("rejected") + end + it "returns `planned` if the :funding_status is 'planned'" do + plan = build(:plan, funding_status: 'planned') + expect(described_class.status(plan: plan)).to eql("planned") end - end -end +end \ No newline at end of file From 834fe69ef4b68ccde83b84fe479bec97337a43a4 Mon Sep 17 00:00:00 2001 From: briri Date: Mon, 9 Aug 2021 10:30:24 -0700 Subject: [PATCH 06/59] appeasing rubocop --- spec/presenters/api/v1/funding_presenter_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/presenters/api/v1/funding_presenter_spec.rb b/spec/presenters/api/v1/funding_presenter_spec.rb index 5005df7a20..03815cf586 100644 --- a/spec/presenters/api/v1/funding_presenter_spec.rb +++ b/spec/presenters/api/v1/funding_presenter_spec.rb @@ -13,15 +13,15 @@ expect(described_class.status(plan: plan)).to eql("planned") end it "returns `granted` if the :funding_status is 'funded'" do - plan = build(:plan, funding_status: 'funded') + plan = build(:plan, funding_status: "funded") expect(described_class.status(plan: plan)).to eql("granted") end it "returns `rejected` if the :funding_status is 'denied'" do - plan = build(:plan, funding_status: 'denied') + plan = build(:plan, funding_status: "denied") expect(described_class.status(plan: plan)).to eql("rejected") end it "returns `planned` if the :funding_status is 'planned'" do - plan = build(:plan, funding_status: 'planned') + plan = build(:plan, funding_status: "planned") expect(described_class.status(plan: plan)).to eql("planned") end end From df6e8a258febaf25ae5efc7b380bd76d1f52fafa Mon Sep 17 00:00:00 2001 From: briri Date: Mon, 9 Aug 2021 10:40:52 -0700 Subject: [PATCH 07/59] appeasing rubocop --- spec/presenters/api/v1/funding_presenter_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/presenters/api/v1/funding_presenter_spec.rb b/spec/presenters/api/v1/funding_presenter_spec.rb index 03815cf586..7deda34a1d 100644 --- a/spec/presenters/api/v1/funding_presenter_spec.rb +++ b/spec/presenters/api/v1/funding_presenter_spec.rb @@ -26,4 +26,4 @@ end end -end \ No newline at end of file +end From 8d5b95436dc8960c5d4a639b816c87cf56c04edb Mon Sep 17 00:00:00 2001 From: briri Date: Tue, 21 Sep 2021 09:50:12 -0700 Subject: [PATCH 08/59] hopeful fix for randomly failing spec --- spec/services/api/v1/persistence_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/api/v1/persistence_service_spec.rb b/spec/services/api/v1/persistence_service_spec.rb index bfb9167152..1c3e4278fe 100644 --- a/spec/services/api/v1/persistence_service_spec.rb +++ b/spec/services/api/v1/persistence_service_spec.rb @@ -185,7 +185,7 @@ expect(results.length).to eql(0) end it "leaves different :contributors as-is" do - @plan.contributors << build(:contributor, name: Faker::Movies::StarWars.character, + @plan.contributors << build(:contributor, name: Faker::Movies::StarWars.unique.character, email: Faker::Internet.unique.email) results = described_class.send(:deduplicate_contributors, contributors: @plan.contributors) expect(results.length).to eql(2) From 8a9cd8f4dda90d0f6e2245dbbd3df803ddd1371c Mon Sep 17 00:00:00 2001 From: briri Date: Wed, 2 Feb 2022 08:36:28 -0800 Subject: [PATCH 09/59] fixed rubocop --- app/controllers/plan_exports_controller.rb | 8 +- app/controllers/plans_controller.rb | 1 - .../research_outputs_controller.rb | 37 ++-- app/models/application_record.rb | 5 +- app/models/license.rb | 8 +- app/models/metadata_standard.rb | 4 +- app/models/plan.rb | 4 +- app/models/repository.rb | 14 +- app/policies/research_output_policy.rb | 9 +- app/presenters/api/v1/api_presenter.rb | 13 +- .../api/v1/research_output_presenter.rb | 19 +- app/presenters/research_output_presenter.rb | 77 ++++---- app/services/external_apis/rdamsc_service.rb | 22 +-- app/services/external_apis/re3data_service.rb | 54 +++--- app/services/external_apis/spdx_service.rb | 24 +-- config/initializers/_dmproadmap.rb | 36 ++++ lib/tasks/utils/external_api.rake | 16 +- spec/factories/orgs.rb | 2 +- spec/features/modal_search_spec.rb | 30 ++- spec/models/license_spec.rb | 25 ++- spec/models/metadata_standard_spec.rb | 19 +- spec/models/repository_spec.rb | 31 ++-- spec/models/research_output_spec.rb | 19 +- .../api/v1/funding_presenter_spec.rb | 25 ++- .../research_output_presenter_spec.rb | 114 ++++++------ .../api/v1/persistence_service_spec.rb | 2 +- .../external_apis/rdamsc_service_spec.rb | 74 ++++---- .../external_apis/re3data_service_spec.rb | 175 +++++++++--------- .../external_apis/spdx_service_spec.rb | 54 +++--- spec/support/helpers/webmocks.rb | 4 +- .../v1/datasets/_show.json.jbuilder_spec.rb | 77 ++++---- .../modal_search/_form.html.erb_spec.rb | 37 ++-- .../modal_search/_result.html.erb_spec.rb | 38 ++-- .../modal_search/_results.html.erb_spec.rb | 40 ++-- .../modal_search/_selections.html.erb_spec.rb | 18 +- 35 files changed, 564 insertions(+), 571 deletions(-) diff --git a/app/controllers/plan_exports_controller.rb b/app/controllers/plan_exports_controller.rb index 4296de5e6d..4347b2907a 100644 --- a/app/controllers/plan_exports_controller.rb +++ b/app/controllers/plan_exports_controller.rb @@ -6,7 +6,8 @@ class PlanExportsController < ApplicationController include ConditionsHelper - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity + # 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]) @@ -51,7 +52,8 @@ def show format.json { show_json } end end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity private @@ -129,6 +131,6 @@ def export_params 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]]) + 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 4fd8770f6c..7443f100b1 100644 --- a/app/controllers/plans_controller.rb +++ b/app/controllers/plans_controller.rb @@ -535,6 +535,5 @@ def render_phases_edit(plan, phase, guidance_groups) guidance_presenter: GuidancePresenter.new(plan) }) end - end # rubocop:enable Metrics/ClassLength diff --git a/app/controllers/research_outputs_controller.rb b/app/controllers/research_outputs_controller.rb index 67955689bc..c05927b20c 100644 --- a/app/controllers/research_outputs_controller.rb +++ b/app/controllers/research_outputs_controller.rb @@ -1,7 +1,7 @@ # 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 @@ -19,7 +19,7 @@ def index # GET /plans/:plan_id/research_outputs/new def new - @research_output = ResearchOutput.new(plan_id: @plan.id, output_type: "") + @research_output = ResearchOutput.new(plan_id: @plan.id, output_type: '') authorize @research_output end @@ -37,14 +37,15 @@ def create if @research_output.save redirect_to plan_research_outputs_path(@plan), - notice: success_message(@research_output, _("added")) + notice: success_message(@research_output, _('added')) else - flash[:alert] = failure_message(@research_output, _("add")) - render "research_outputs/new" + 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) @@ -56,12 +57,13 @@ def update if @research_output.update(args) redirect_to plan_research_outputs_path(@plan), - notice: success_message(@research_output, _("saved")) + notice: success_message(@research_output, _('saved')) else redirect_to edit_plan_research_output_path(@plan, @research_output), - alert: failure_message(@research_output, _("save")) + alert: failure_message(@research_output, _('save')) end end + # rubocop:enable Metrics/AbcSize # DELETE /plans/:plan_id/research_outputs/:id def destroy @@ -69,10 +71,10 @@ def destroy if @research_output.destroy redirect_to plan_research_outputs_path(@plan), - notice: success_message(@research_output, _("removed")) + notice: success_message(@research_output, _('removed')) else redirect_to plan_research_outputs_path(@plan), - alert: failure_message(@research_output, _("remove")) + alert: failure_message(@research_output, _('remove')) end end @@ -99,6 +101,7 @@ def select_license 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) @@ -110,6 +113,7 @@ def repository_search @search_results = @search_results.order(:name).page(params[:page]) end + # rubocop:enable Metrics/AbcSize # PUT /plans/:id/repository_select def repository_select @@ -157,18 +161,19 @@ 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" + when 'pb' args[:file_size].to_f.petabytes - when "tb" + when 'tb' args[:file_size].to_f.terabytes - when "gb" + when 'gb' args[:file_size].to_f.gigabytes - when "mb" + when 'mb' args[:file_size].to_f.megabytes else args[:file_size].to_i @@ -181,6 +186,7 @@ def process_byte_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 @@ -199,7 +205,7 @@ def fetch_plan @plan = Plan.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_research_output @@ -208,7 +214,6 @@ def fetch_research_output return true if @research_output.present? && @plan.research_outputs.include?(@research_output) - redirect_to plan_research_outputs_path, alert: _("research output not found") + redirect_to plan_research_outputs_path, alert: _('research output not found') end - end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 0f75a110bd..fc9717e959 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -11,11 +11,11 @@ class ApplicationRecord < ActiveRecord::Base class << self # Indicates whether the underlying DB is MySQL def mysql_db? - ActiveRecord::Base.connection.adapter_name == "Mysql2" + ActiveRecord::Base.connection.adapter_name == 'Mysql2' end def postgres_db? - ActiveRecord::Base.connection.adapter_name == "PostgreSQL" + ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' end # Generates the appropriate where clause for a JSON field based on the DB type @@ -31,7 +31,6 @@ def safe_regexp_where_clause(column:) "#{column} REGEXP ?" end - end def sanitize_fields(*attrs) diff --git a/app/models/license.rb b/app/models/license.rb index cc8bd067da..0c5cd49c3e 100644 --- a/app/models/license.rb +++ b/app/models/license.rb @@ -20,7 +20,6 @@ # index_licenses_on_uri (uri) # class License < ApplicationRecord - # ================ # = Associations = # ================ @@ -42,13 +41,12 @@ class License < ApplicationRecord 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) + pref = preference.gsub('%s', '[0-9\\.]+$') + where_clause = safe_regexp_where_clause(column: 'identifier') + rslts = preference.include?('%s') ? 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 } - end diff --git a/app/models/metadata_standard.rb b/app/models/metadata_standard.rb index d8a3f1e756..ea8f69c065 100644 --- a/app/models/metadata_standard.rb +++ b/app/models/metadata_standard.rb @@ -15,7 +15,6 @@ # rdamsc_id :string # class MetadataStandard < ApplicationRecord - # ================ # = Associations = # ================ @@ -28,7 +27,6 @@ class MetadataStandard < ApplicationRecord scope :search, lambda { |term| term = term.downcase - where("LOWER(title) LIKE ?", "%#{term}%").or(where("LOWER(description) LIKE ?", "%#{term}%")) + where('LOWER(title) LIKE ?', "%#{term}%").or(where('LOWER(description) LIKE ?', "%#{term}%")) } - end diff --git a/app/models/plan.rb b/app/models/plan.rb index 056cafc757..a30d9a9342 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -43,6 +43,7 @@ # # Object that represents an DMP +# rubocop:disable Metrics/ClassLength class Plan < ApplicationRecord include ConditionalUserMailer include ExportablePlan @@ -119,7 +120,7 @@ class Plan < ApplicationRecord has_many :contributors, dependent: :destroy - has_one :grant, as: :identifiable, dependent: :destroy, class_name: "Identifier" + has_one :grant, as: :identifiable, dependent: :destroy, class_name: 'Identifier' has_many :research_outputs, dependent: :destroy @@ -616,3 +617,4 @@ def end_date_after_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/repository.rb b/app/models/repository.rb index 06ffad7588..5db15716eb 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -22,7 +22,6 @@ # class Repository < ApplicationRecord - # ================ # = Associations = # ================ @@ -34,23 +33,22 @@ class Repository < ApplicationRecord # ========== scope :by_type, lambda { |type| - where(safe_json_where_clause(column: "info", hash_key: "types"), "%#{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}%") + 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}%")) + 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}%") + where(safe_json_where_clause(column: 'info', hash_key: 'keywords'), "%#{facet}%") } - end diff --git a/app/policies/research_output_policy.rb b/app/policies/research_output_policy.rb index 8b79ddf0bb..defe86a0c9 100644 --- a/app/policies/research_output_policy.rb +++ b/app/policies/research_output_policy.rb @@ -1,15 +1,13 @@ # 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, _('must be logged in') unless user - unless research_output.present? - raise Pundit::NotAuthorizedError, _("are not authorized to view that plan") - end + raise Pundit::NotAuthorizedError, _('are not authorized to view that plan') unless research_output.present? @user = user @research_output = research_output @@ -55,5 +53,4 @@ def repository_search? def metadata_standard_search? @research_output.plan.administerable_by?(@user.id) end - end diff --git a/app/presenters/api/v1/api_presenter.rb b/app/presenters/api/v1/api_presenter.rb index 272b3f5d61..692adef0cf 100644 --- a/app/presenters/api/v1/api_presenter.rb +++ b/app/presenters/api/v1/api_presenter.rb @@ -1,23 +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? + return 'unknown' unless value.present? - value ? "yes" : "no" + value ? 'yes' : 'no' 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 index 851e5837da..28b7325312 100644 --- a/app/presenters/api/v1/research_output_presenter.rb +++ b/app/presenters/api/v1/research_output_presenter.rb @@ -1,11 +1,9 @@ # 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 @@ -35,9 +33,9 @@ def determine_license_start_date(output:) end def load_narrative_content - @preservation_statement = "" + @preservation_statement = '' @security_and_privacy = [] - @data_quality_assurance = "" + @data_quality_assurance = '' # Disabling rubocop here since a guard clause would make the line too long # rubocop:disable Style/GuardClause @@ -45,18 +43,19 @@ def load_narrative_content @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"]) + @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"]) + @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("
    ") + 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? @@ -72,9 +71,7 @@ def fetch_q_and_a(themes:) end ret.select { |item| item[:description].present? } end - + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity end - end - end diff --git a/app/presenters/research_output_presenter.rb b/app/presenters/research_output_presenter.rb index 74f0d007f1..3a77f1be80 100644 --- a/app/presenters/research_output_presenter.rb +++ b/app/presenters/research_output_presenter.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true +# Helper methods for the research outputs tab class ResearchOutputPresenter - attr_accessor :research_output def initialize(research_output:) @@ -22,7 +22,7 @@ def selectable_access_types # Returns the options for file size units def selectable_size_units - [%w[MB mb], %w[GB gb], %w[TB tb], %w[PB pb], ["bytes", ""]] + [%w[MB mb], %w[GB gb], %w[TB tb], %w[PB pb], ['bytes', '']] end # Returns the options for metadata standards @@ -30,7 +30,7 @@ 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")) + MetadataStandard.where(descipline_specific: (category == 'disciplinary')) .map { |ms| [ms.title, ms.id] } end @@ -56,48 +56,50 @@ def byte_sizable? # 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" + '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("-", " ")] + [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"] + [_('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 + 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: "" } + { 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 '' unless @research_output.is_a?(ResearchOutput) return "#{@research_output.title[0..49]} ..." if @research_output.title.length > 50 @research_output.title @@ -105,53 +107,52 @@ def display_name # Returns the humanized version of the output_type enum variable def display_type - return "" unless @research_output.is_a?(ResearchOutput) + 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 + @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? + 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? + 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? + 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? + 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? + 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? + return 'Unspecified' if value.nil? - value ? "Yes" : "No" + value ? 'Yes' : 'No' end - end diff --git a/app/services/external_apis/rdamsc_service.rb b/app/services/external_apis/rdamsc_service.rb index dbadb74a34..b6299a1a2a 100644 --- a/app/services/external_apis/rdamsc_service.rb +++ b/app/services/external_apis/rdamsc_service.rb @@ -1,7 +1,6 @@ # 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 @@ -11,9 +10,7 @@ module ExternalApis # 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 @@ -105,9 +102,9 @@ def query_schemes(path:) return false unless json.present? process_scheme_entries(json: json) - return true unless json.fetch("data", {})["nextLink"].present? + return true unless json.fetch('data', {})['nextLink'].present? - query_schemes(path: json["data"]["nextLink"]) + query_schemes(path: json['data']['nextLink']) end def query_api(path:) @@ -126,21 +123,20 @@ def query_api(path:) 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? + 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"]) + 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 index f37bbc212f..35337e4e70 100644 --- a/app/services/external_apis/re3data_service.rb +++ b/app/services/external_apis/re3data_service.rb @@ -1,14 +1,11 @@ # 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 @@ -55,13 +52,13 @@ def fetch xml_list = query_re3data return [] unless xml_list.present? - xml_list.xpath("/list/repository/id").each do |node| + 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) + process_repository(id: node.text, node: xml.xpath('//r3d:re3data//r3d:repository').first) end end @@ -74,10 +71,10 @@ def query_re3data debug: false) unless resp.present? && resp.code == 200 - handle_http_failure(method: "re3data list", http_response: resp) + handle_http_failure(method: 're3data list', http_response: resp) return nil end - Nokogiri.XML(resp.body, nil, "utf8") + Nokogiri.XML(resp.body, nil, 'utf8') end # Queries the re3data API for the specified repository @@ -93,7 +90,7 @@ def query_re3data_repository(repo_id:) handle_http_failure(method: "re3data repository #{repo_id}", http_response: resp) return [] end - Nokogiri.XML(resp.body, nil, "utf8") + Nokogiri.XML(resp.body, nil, 'utf8') end # Updates or Creates a repository based on the XML input @@ -102,8 +99,8 @@ def process_repository(id:, node:) # 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 + 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) @@ -111,35 +108,35 @@ def process_repository(id:, node:) end # Updates the Repository based on the XML input - # rubocop:disable Metrics/AbcSize + # 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, + 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) } + 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 + # 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 + name: node.xpath('r3d:policyName')&.text, + url: node.xpath('r3d:policyURL')&.text } end @@ -147,13 +144,10 @@ def parse_upload(node:) return nil unless node.present? { - type: node.xpath("r3d:dataUploadType")&.text, - restriction: node.xpath("r3d:dataUploadRestriction")&.text + type: node.xpath('r3d:dataUploadType')&.text, + restriction: node.xpath('r3d:dataUploadRestriction')&.text } end - end - end - end diff --git a/app/services/external_apis/spdx_service.rb b/app/services/external_apis/spdx_service.rb index 363d2e5b4e..054e96d2a9 100644 --- a/app/services/external_apis/spdx_service.rb +++ b/app/services/external_apis/spdx_service.rb @@ -1,13 +1,10 @@ # 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 @@ -69,15 +66,15 @@ def query_spdx 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) + handle_http_failure(method: 'SPDX list', http_response: resp) return [] end json = JSON.parse(resp.body) - return [] unless json.fetch("licenses", []).any? + return [] unless json.fetch('licenses', []).any? - json["licenses"] + json['licenses'] rescue JSON::ParserError => e - log_error(method: "SPDX search", error: e) + log_error(method: 'SPDX search', error: e) [] end @@ -86,19 +83,16 @@ def process_license(hash:) return nil unless hash.present? hash = hash.with_indifferent_access - license = License.find_or_initialize_by(identifier: hash["licenseId"]) + 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"] + name: hash['name'], + uri: hash['detailsUrl'], + osi_approved: hash['isOsiApproved'], + deprecated: hash['isDeprecatedLicenseId'] ) end - end - end - end diff --git a/config/initializers/_dmproadmap.rb b/config/initializers/_dmproadmap.rb index 875bc8a38d..76a9565bb0 100644 --- a/config/initializers/_dmproadmap.rb +++ b/config/initializers/_dmproadmap.rb @@ -212,5 +212,41 @@ class Application < Rails::Application # Enable/disable functionality on the Project Details tab config.x.madmp.enable_ethical_issues = false config.x.madmp.enable_research_domain = false + + # This flag will enable/disable the entire Research Outputs tab. The others below will + # just enable/disable specific functionality on the Research Outputs tab + config.x.madmp.enable_research_outputs = false + config.x.madmp.enable_license_selection = false + config.x.madmp.enable_metadata_standard_selection = false + config.x.madmp.enable_repository_selection = false + + # The following flags will allow the system to include the question and answer in the JSON output + # - questions with a theme equal to 'Preservation' + config.x.madmp.extract_preservation_statements_from_themed_questions = false + # - questions with a theme equal to 'Data Collection' + config.x.madmp.extract_data_quality_statements_from_themed_questions = false + # - questions with a theme equal to 'Ethics & privacy' or 'Storage & security' + config.x.madmp.extract_security_privacy_statements_from_themed_questions = false + + # Specify a list of the preferred licenses types. These licenses will appear in a select + # box on the 'Research Outputs' tab when editing a plan along with the option to select + # 'other'. When 'other' is selected, the user is presented with the full list of licenses. + # + # The licenses will appear in the order you specify here. + # + # Note that the values you enter must match the :identifier field of the licenses table. + # You can use the `%{latest}` markup in place of version numbers if desired. + config.x.madmp.preferred_licenses = [ + 'CC-BY-%s', + 'CC-BY-SA-%s', + 'CC-BY-NC-%s', + 'CC-BY-NC-SA-%s', + 'CC-BY-ND-%s', + 'CC-BY-NC-ND-%s', + 'CC0-%s' + ] + # Link to external guidance about selecting one of the preferred licenses. A default + # URL will be displayed if none is provided here. See app/views/research_outputs/licenses/_form + config.x.madmp.preferred_licenses_guidance_url = 'https://creativecommons.org/about/cclicenses/' end end diff --git a/lib/tasks/utils/external_api.rake b/lib/tasks/utils/external_api.rake index d69c5086e7..7191d94621 100644 --- a/lib/tasks/utils/external_api.rake +++ b/lib/tasks/utils/external_api.rake @@ -1,26 +1,26 @@ # frozen_string_literal: true namespace :external_api do - desc "Fetch the latest RDA Metadata Standards" + desc 'Fetch the latest RDA Metadata Standards' task load_rdamsc_standards: :environment do - p "Fetching the latest RDAMSC metadata standards and updating the metadata_standards table" + p 'Fetching the latest RDAMSC metadata standards and updating the metadata_standards table' ExternalApis::RdamscService.fetch_metadata_standards end - desc "Load Repositories from re3data" + desc 'Load Repositories from re3data' task load_re3data_repos: :environment do - p "Fetching the latest re3data repository metadata and updating the repositories table" - p "This can take in excess of 10 minutes to complete ..." + p 'Fetching the latest re3data repository metadata and updating the repositories table' + p 'This can take in excess of 10 minutes to complete ...' ExternalApis::Re3dataService.fetch end - desc "Load Licenses from SPDX" + desc 'Load Licenses from SPDX' task load_spdx_licenses: :environment do - p "Fetching the latest SPDX license metadata and updating the licenses table" + p 'Fetching the latest SPDX license metadata and updating the licenses table' ExternalApis::SpdxService.fetch end - desc "Seed the Research Domain table with Field of Science categories" + desc 'Seed the Research Domain table with Field of Science categories' task add_field_of_science_to_research_domains: :environment do # TODO: If we can identify an external API authority for this information we should switch # to fetch the list from there instead of the hard-coded list below which was derived from: diff --git a/spec/factories/orgs.rb b/spec/factories/orgs.rb index 2f569c6c0f..a9f15f74b5 100644 --- a/spec/factories/orgs.rb +++ b/spec/factories/orgs.rb @@ -34,7 +34,7 @@ FactoryBot.define do factory :org do name { Faker::Company.unique.name } - links { { "org" => [] } } + links { { 'org' => [] } } abbreviation { SecureRandom.hex(6) } feedback_enabled { false } region { Region.first || create(:region) } diff --git a/spec/features/modal_search_spec.rb b/spec/features/modal_search_spec.rb index 8431abbdca..9bc7523aac 100644 --- a/spec/features/modal_search_spec.rb +++ b/spec/features/modal_search_spec.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require "rails_helper" - -RSpec.feature "ModalSearchDialog", type: :feature do +require 'rails_helper' +RSpec.feature 'ModalSearchDialog', type: :feature do include Webmocks before(:each) do @@ -19,35 +18,34 @@ Rails.configuration.x.madmp.enable_repository_selection = true click_link @plan.title - click_link "Research Outputs" - click_link "Add a research output" + click_link 'Research Outputs' + click_link 'Add a research output' end - it "Modal search opens and closes and allows user to search, select and remove items", :js do + it 'Modal search opens and closes and allows user to search, select and remove items', :js do # Open the modal - click_button "Add a repository" - expect(page).to have_text("Repository search") + click_button 'Add a repository' + expect(page).to have_text('Repository search') - within("#modal-search-repositories") do + within('#modal-search-repositories') do # Search for the Repository - fill_in "research_output_search_term", with: @model.name - click_button "Apply filter(s)" + fill_in 'research_output_search_term', with: @model.name + click_button 'Apply filter(s)' expect(page).to have_text(@model.description) # Select the repository and make sure it no longer appears in the search results - click_link "Select" + click_link 'Select' expect(page).not_to have_text(@model.description) # Close the modal - click_button "Close" + click_button 'Close' end # Verify that the selection was added to the main page's dom - expect(page).not_to have_text("Repository search") + expect(page).not_to have_text('Repository search') expect(page).to have_text(@model.description) # Verify that we can remove the selection - click_link "Remove" + click_link 'Remove' expect(page).not_to have_text(@model.description) end - end diff --git a/spec/models/license_spec.rb b/spec/models/license_spec.rb index 15771c78bf..f51785ad22 100644 --- a/spec/models/license_spec.rb +++ b/spec/models/license_spec.rb @@ -1,29 +1,28 @@ # frozen_string_literal: true -require "rails_helper" +require 'rails_helper' describe License do - - context "associations" do + context 'associations' do it { is_expected.to have_many :research_outputs } end - context "scopes" do - describe "#selectable" do + context 'scopes' do + describe '#selectable' do before(:each) do @license = create(:license, deprecated: false) @deprecated = create(:license, deprecated: true) end - it "does not include deprecated licenses" do + it 'does not include deprecated licenses' do expect(described_class.selectable.include?(@deprecated)).to eql(false) end - it "includes non-depracated licenses" do + it 'includes non-depracated licenses' do expect(described_class.selectable.include?(@license)).to eql(true) end end - describe "#preferred" do + describe '#preferred' do before(:each) do @preferred_license = create(:license, deprecated: false) @non_preferred_license = create(:license, deprecated: false) @@ -36,22 +35,22 @@ Rails.configuration.x.madmp.preferred_licenses = [ @preferred_license.identifier, - "#{@preferred_oldest.identifier}-%{latest}" + "#{@preferred_oldest.identifier}-%s" ] end - it "calls :selectable if no preferences are defined in the app config" do + it 'calls :selectable if no preferences are defined in the app config' do Rails.configuration.x.madmp.preferred_licenses = nil described_class.expects(:selectable).returns([@license]) described_class.preferred end - it "does not include non-preferred licenses" do + it 'does not include non-preferred licenses' do expect(described_class.preferred.include?(@non_preferred_license)).to eql(false) end - it "includes preferred licenses" do + it 'includes preferred licenses' do expect(described_class.preferred.include?(@preferred_license)).to eql(true) end - it "includes the latest version of a preferred licenses" do + it 'includes the latest version of a preferred licenses' do expect(described_class.preferred.include?(@preferred_latest)).to eql(true) expect(described_class.preferred.include?(@preferred_oldest)).to eql(false) expect(described_class.preferred.include?(@preferred_older)).to eql(false) diff --git a/spec/models/metadata_standard_spec.rb b/spec/models/metadata_standard_spec.rb index a3ea22babb..5d48541c10 100644 --- a/spec/models/metadata_standard_spec.rb +++ b/spec/models/metadata_standard_spec.rb @@ -1,31 +1,28 @@ # frozen_string_literal: true -require "rails_helper" +require 'rails_helper' describe MetadataStandard do - - context "associations" do + context 'associations' do it { is_expected.to have_and_belong_to_many :research_outputs } end - context "scopes" do + context 'scopes' do before(:each) do - @name_part = "Foobar" - @by_title = create(:metadata_standard, title: [Faker::Lorem.sentence, @name_part].join(" ")) - desc = [@name_part, Faker::Lorem.paragraph].join(" ") + @name_part = 'Foobar' + @by_title = create(:metadata_standard, title: [Faker::Lorem.sentence, @name_part].join(' ')) + desc = [@name_part, Faker::Lorem.paragraph].join(' ') @by_description = create(:metadata_standard, description: desc) end - it ":search returns the expected records" do + it ':search returns the expected records' do results = described_class.search(@name_part) expect(results.include?(@by_title)).to eql(true) expect(results.include?(@by_description)).to eql(true) - results = described_class.search("Zzzzzz") + results = described_class.search('Zzzzzz') expect(results.include?(@by_title)).to eql(false) expect(results.include?(@by_description)).to eql(false) end - end - end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 278d6f7861..09f76fc861 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -19,21 +19,20 @@ # index_repositories_on_url (url) # -require "rails_helper" +require 'rails_helper' describe Repository do - - context "associations" do + context 'associations' do it { is_expected.to have_and_belong_to_many :research_outputs } end - context "scopes" do + context 'scopes' do before(:each) do @types = %w[Armadillo Barracuda] @subjects = %w[Capybara Dingo] @keywords = %w[Elephant Falcon] - @never_found = create(:repository, name: "foo", info: { types: [@types.last], + @never_found = create(:repository, name: 'foo', info: { types: [@types.last], subjects: [@subjects.last], keywords: [@keywords.last] }) @@ -48,8 +47,8 @@ keywords: [@keywords.first] }) end - describe "#by_type" do - it "returns the expected repositories" do + describe '#by_type' do + it 'returns the expected repositories' do results = described_class.by_type(@types.first) expect(results.include?(@never_found)).to eql(false) expect(results.include?(@by_type)).to eql(true) @@ -58,8 +57,8 @@ end end - describe "#by_subject" do - it "returns the expected repositories" do + describe '#by_subject' do + it 'returns the expected repositories' do results = described_class.by_subject(@subjects.first) expect(results.include?(@never_found)).to eql(false) expect(results.include?(@by_type)).to eql(false) @@ -68,8 +67,8 @@ end end - describe "#by_facet" do - it "returns the expected repositories" do + describe '#by_facet' do + it 'returns the expected repositories' do results = described_class.by_facet(@keywords.first) expect(results.include?(@never_found)).to eql(false) expect(results.include?(@by_type)).to eql(false) @@ -78,22 +77,22 @@ end end - describe "#search" do - it "returns repositories with keywords like the search term" do + describe '#search' do + it 'returns repositories with keywords like the search term' do results = described_class.search(@keywords.first[1..3]) expect(results.include?(@never_found)).to eql(false) expect(results.include?(@by_type)).to eql(false) expect(results.include?(@by_subject)).to eql(false) expect(results.include?(@by_facet)).to eql(true) end - it "returns repositories with subjects like the search term" do + it 'returns repositories with subjects like the search term' do results = described_class.search(@by_subject.name[1..@by_subject.name.length - 1]) expect(results.include?(@never_found)).to eql(false) expect(results.include?(@by_type)).to eql(false) expect(results.include?(@by_subject)).to eql(true) end - it "returns repositories with name like the search term" do - repo = create(:repository, name: [Faker::Lorem.word, @by_subject.name].join(" ")) + it 'returns repositories with name like the search term' do + repo = create(:repository, name: [Faker::Lorem.word, @by_subject.name].join(' ')) results = described_class.search(@by_subject.name[1..@by_subject.name.length - 1]) expect(results.include?(@never_found)).to eql(false) expect(results.include?(repo)).to eql(true) diff --git a/spec/models/research_output_spec.rb b/spec/models/research_output_spec.rb index 57247c258e..f09baf1d1e 100644 --- a/spec/models/research_output_spec.rb +++ b/spec/models/research_output_spec.rb @@ -3,8 +3,7 @@ require 'rails_helper' RSpec.describe ResearchOutput, type: :model do - - context "associations" do + context 'associations' do it { is_expected.to belong_to(:plan).optional.touch(true) } end @@ -19,8 +18,16 @@ it { is_expected.to validate_presence_of(:access) } it { is_expected.to validate_presence_of(:title) } - it { expect(@subject).to validate_uniqueness_of(:title).case_insensitive.scoped_to(:plan_id).with_message("must be unique") } - it { expect(@subject).to validate_uniqueness_of(:abbreviation).case_insensitive.scoped_to(:plan_id).with_message("must be unique") } + it { + expect(@subject).to validate_uniqueness_of(:title).case_insensitive + .scoped_to(:plan_id) + .with_message('must be unique') + } + it { + expect(@subject).to validate_uniqueness_of(:abbreviation).case_insensitive + .scoped_to(:plan_id) + .with_message('must be unique') + } it "requires :output_type_description if :output_type is 'other'" do @subject.other! @@ -35,8 +42,8 @@ expect(build(:research_output).valid?).to eql(true) end - describe "cascading deletes" do - it "does not delete associated plan" do + describe 'cascading deletes' do + it 'does not delete associated plan' do model = create(:research_output, plan: create(:plan)) plan = model.plan model.destroy diff --git a/spec/presenters/api/v1/funding_presenter_spec.rb b/spec/presenters/api/v1/funding_presenter_spec.rb index 80a6dd1b3f..87c3cbeda8 100644 --- a/spec/presenters/api/v1/funding_presenter_spec.rb +++ b/spec/presenters/api/v1/funding_presenter_spec.rb @@ -1,28 +1,27 @@ # frozen_string_literal: true -require "rails_helper" +require 'rails_helper' RSpec.describe Api::V1::FundingPresenter do - - describe "#status(plan:)" do - it "returns `planned` if the plan is nil" do - expect(described_class.status(plan: nil)).to eql("planned") + describe '#status(plan:)' do + it 'returns `planned` if the plan is nil' do + expect(described_class.status(plan: nil)).to eql('planned') end - it "returns `planned` if the :funding_status is nil" do + it 'returns `planned` if the :funding_status is nil' do plan = build(:plan, funding_status: nil) - expect(described_class.status(plan: plan)).to eql("planned") + expect(described_class.status(plan: plan)).to eql('planned') end it "returns `granted` if the :funding_status is 'funded'" do - plan = build(:plan, funding_status: "funded") - expect(described_class.status(plan: plan)).to eql("granted") + plan = build(:plan, funding_status: 'funded') + expect(described_class.status(plan: plan)).to eql('granted') end it "returns `rejected` if the :funding_status is 'denied'" do - plan = build(:plan, funding_status: "denied") - expect(described_class.status(plan: plan)).to eql("rejected") + plan = build(:plan, funding_status: 'denied') + expect(described_class.status(plan: plan)).to eql('rejected') end it "returns `planned` if the :funding_status is 'planned'" do - plan = build(:plan, funding_status: "planned") - expect(described_class.status(plan: plan)).to eql("planned") + plan = build(:plan, funding_status: 'planned') + expect(described_class.status(plan: plan)).to eql('planned') end end end diff --git a/spec/presenters/research_output_presenter_spec.rb b/spec/presenters/research_output_presenter_spec.rb index 3bf65a1925..43b84ec311 100644 --- a/spec/presenters/research_output_presenter_spec.rb +++ b/spec/presenters/research_output_presenter_spec.rb @@ -1,16 +1,15 @@ # frozen_string_literal: true -require "rails_helper" +require 'rails_helper' RSpec.describe ResearchOutputPresenter do - before(:each) do @research_output = create(:research_output, plan: create(:plan)) @presenter = described_class.new(research_output: @research_output) end - describe ":selectable_output_types" do - it "returns the output types" do + describe ':selectable_output_types' do + it 'returns the output types' do expect(@presenter.selectable_output_types.any?).to eql(true) end it "packages the output types for a selectbox - [['Audiovisual', 'audiovisual']]" do @@ -23,8 +22,8 @@ end end - describe ":selectable_access_types" do - it "returns the output types" do + describe ':selectable_access_types' do + it 'returns the output types' do expect(@presenter.selectable_access_types.any?).to eql(true) end it "packages the output types for a selectbox - [['Open', 'open']]" do @@ -37,8 +36,8 @@ end end - describe ":selectable_size_units" do - it "returns the output types" do + describe ':selectable_size_units' do + it 'returns the output types' do expect(@presenter.selectable_size_units.any?).to eql(true) end it "packages the output types for a selectbox - [['MB', 'mb']]" do @@ -50,80 +49,80 @@ end end - describe ":converted_file_size(size:)" do - it "returns an zero MB if size is not present" do - expect(@presenter.converted_file_size(size: nil)).to eql({ size: nil, unit: "mb" }) + describe ':converted_file_size(size:)' do + it 'returns an zero MB if size is not present' do + expect(@presenter.converted_file_size(size: nil)).to eql({ size: nil, unit: 'mb' }) end - it "returns an zero MB if size is not a number" do - expect(@presenter.converted_file_size(size: "foo")).to eql({ size: nil, unit: "mb" }) + it 'returns an zero MB if size is not a number' do + expect(@presenter.converted_file_size(size: 'foo')).to eql({ size: nil, unit: 'mb' }) end - it "returns an zero MB if size is not positive" do - expect(@presenter.converted_file_size(size: -1)).to eql({ size: nil, unit: "mb" }) + it 'returns an zero MB if size is not positive' do + expect(@presenter.converted_file_size(size: -1)).to eql({ size: nil, unit: 'mb' }) end - it "can handle bytes" do - expect(@presenter.converted_file_size(size: 100)).to eql({ size: 100, unit: "" }) + it 'can handle bytes' do + expect(@presenter.converted_file_size(size: 100)).to eql({ size: 100, unit: '' }) end - it "can handle megabytes" do - expect(@presenter.converted_file_size(size: 1.megabytes)).to eql({ size: 1, unit: "mb" }) + it 'can handle megabytes' do + expect(@presenter.converted_file_size(size: 1.megabytes)).to eql({ size: 1, unit: 'mb' }) end - it "can handle gigabytes" do - expect(@presenter.converted_file_size(size: 1.gigabytes)).to eql({ size: 1, unit: "gb" }) + it 'can handle gigabytes' do + expect(@presenter.converted_file_size(size: 1.gigabytes)).to eql({ size: 1, unit: 'gb' }) end - it "can handle terabytes" do - expect(@presenter.converted_file_size(size: 1.terabytes)).to eql({ size: 1, unit: "tb" }) + it 'can handle terabytes' do + expect(@presenter.converted_file_size(size: 1.terabytes)).to eql({ size: 1, unit: 'tb' }) end - it "can handle petabytes" do - expect(@presenter.converted_file_size(size: 1.petabytes)).to eql({ size: 1, unit: "pb" }) + it 'can handle petabytes' do + expect(@presenter.converted_file_size(size: 1.petabytes)).to eql({ size: 1, unit: 'pb' }) end end - describe ":display_name" do - it "returns an empty string unless if we do not have a ResearchOutput" do + describe ':display_name' do + it 'returns an empty string unless if we do not have a ResearchOutput' do presenter = described_class.new(research_output: build(:org)) - expect(presenter.display_name).to eql("") + expect(presenter.display_name).to eql('') end - it "does not trim names that are <= 50 characters" do - presenter = described_class.new(research_output: build(:research_output, title: "a" * 49)) - expect(presenter.display_name).to eql("a" * 49) + it 'does not trim names that are <= 50 characters' do + presenter = described_class.new(research_output: build(:research_output, title: 'a' * 49)) + expect(presenter.display_name).to eql('a' * 49) end - it "does not trims names that are > 50 characters" do - presenter = described_class.new(research_output: build(:research_output, title: "a" * 51)) + it 'does not trims names that are > 50 characters' do + presenter = described_class.new(research_output: build(:research_output, title: 'a' * 51)) expect(presenter.display_name).to eql("#{'a' * 50} ...") end end - describe ":display_type" do - it "returns an empty string unless if we do not have a ResearchOutput" do + describe ':display_type' do + it 'returns an empty string unless if we do not have a ResearchOutput' do presenter = described_class.new(research_output: build(:org)) - expect(presenter.display_type).to eql("") + expect(presenter.display_type).to eql('') end it "returns the user's description if the output_type is other" do - research_output = build(:research_output, output_type: "other", - output_type_description: "foo") + research_output = build(:research_output, output_type: 'other', + output_type_description: 'foo') presenter = described_class.new(research_output: research_output) - expect(presenter.display_type).to eql("foo") + expect(presenter.display_type).to eql('foo') end - it "returns the humanized version of the output_type" do + it 'returns the humanized version of the output_type' do presenter = described_class.new(research_output: build(:research_output, output_type: :image)) - expect(presenter.display_type).to eql("Image") + expect(presenter.display_type).to eql('Image') end end - describe ":display_repository" do + describe ':display_repository' do before(:each) do @research_output.repositories.clear end it "returns ['None specified'] if not repositories are assigned" do presenter = described_class.new(research_output: @research_output) - expect(presenter.display_repository).to eql(["None specified"]) + expect(presenter.display_repository).to eql(['None specified']) end - it "returns an array of names when there is only one repository" do + it 'returns an array of names when there is only one repository' do repo = build(:repository) @research_output.repositories << repo presenter = described_class.new(research_output: @research_output) expect(presenter.display_repository).to eql([repo.name]) end - it "returns an array of names when there are multiple repositories" do + it 'returns an array of names when there are multiple repositories' do repos = [build(:repository), build(:repository)] @research_output.repositories << repos presenter = described_class.new(research_output: @research_output) @@ -131,32 +130,32 @@ end end - describe ":display_access" do + describe ':display_access' do it "returns 'Unspecified' if :access has not been defined" do presenter = described_class.new(research_output: build(:research_output, access: nil)) - expect(presenter.display_access).to eql("Unspecified") + expect(presenter.display_access).to eql('Unspecified') end - it "returns a humanized version of the :access enum selection" do + it 'returns a humanized version of the :access enum selection' do presenter = described_class.new(research_output: build(:research_output, access: :open)) - expect(presenter.display_access).to eql("Open") + expect(presenter.display_access).to eql('Open') end end - describe ":display_release" do + describe ':display_release' do it "returns 'Unspecified' if :access has not been defined" do presenter = described_class.new(research_output: build(:research_output, release_date: nil)) - expect(presenter.display_release).to eql("Unspecified") + expect(presenter.display_release).to eql('Unspecified') end - it "returns a the release_date as a Date" do + it 'returns a the release_date as a Date' do now = Time.now presenter = described_class.new(research_output: build(:research_output, release_date: now)) expect(presenter.display_release.is_a?(Date)).to eql(true) end end - context "class methods" do - describe ":selectable_subjects" do - it "returns subjects" do + context 'class methods' do + describe ':selectable_subjects' do + it 'returns subjects' do expect(described_class.selectable_subjects.any?).to eql(true) end it "packages the subjects for a selectbox - [['Biology', '21 Biology']]" do @@ -168,8 +167,8 @@ end end - describe ":selectable_repository_types" do - it "returns repository types" do + describe ':selectable_repository_types' do + it 'returns repository types' do expect(described_class.selectable_repository_types.any?).to eql(true) end it "packages the repo types for a selectbox - [['Discipline specific', 'disciplinary']]" do @@ -180,5 +179,4 @@ end end end - end diff --git a/spec/services/api/v1/persistence_service_spec.rb b/spec/services/api/v1/persistence_service_spec.rb index 70106798ad..306f04f555 100644 --- a/spec/services/api/v1/persistence_service_spec.rb +++ b/spec/services/api/v1/persistence_service_spec.rb @@ -183,7 +183,7 @@ results = described_class.send(:deduplicate_contributors, contributors: @plan.contributors) expect(results.length).to eql(0) end - it "leaves different :contributors as-is" do + it 'leaves different :contributors as-is' do @plan.contributors << build(:contributor, name: Faker::Movies::StarWars.unique.character, email: Faker::Internet.unique.email) results = described_class.send(:deduplicate_contributors, contributors: @plan.contributors) diff --git a/spec/services/external_apis/rdamsc_service_spec.rb b/spec/services/external_apis/rdamsc_service_spec.rb index ad430a7579..0f134f886c 100644 --- a/spec/services/external_apis/rdamsc_service_spec.rb +++ b/spec/services/external_apis/rdamsc_service_spec.rb @@ -1,79 +1,78 @@ # frozen_string_literal: true -require "rails_helper" +require 'rails_helper' RSpec.describe ExternalApis::RdamscService do - include Webmocks before(:each) do MetadataStandard.all.destroy_all @rdams_results = { - "apiVersion": "2.0.0", - "data": { - "currentItemCount": Faker::Number.number(digits: 2), - "items": [ + apiVersion: '2.0.0', + data: { + currentItemCount: Faker::Number.number(digits: 2), + items: [ { - "description": Faker::Lorem.paragraph, - "keywords": [ + description: Faker::Lorem.paragraph, + keywords: [ Faker::Internet.unique.url ], - "locations": [ - { "type": %w[document website].sample, "url": Faker::Internet.unique.url } + locations: [ + { type: %w[document website].sample, url: Faker::Internet.unique.url } ], - "mscid": "msc:m#{Faker::Number.number(digits: 2)}", - "relatedEntities": [ - { "id": "msc:m#{Faker::Number.number(digits: 2)}", "role": %w[scheme child].sample } + mscid: "msc:m#{Faker::Number.number(digits: 2)}", + relatedEntities: [ + { id: "msc:m#{Faker::Number.number(digits: 2)}", role: %w[scheme child].sample } ], - "slug": SecureRandom.uuid, - "title": Faker::Lorem.sentence, - "uri": Faker::Internet.unique.url + slug: SecureRandom.uuid, + title: Faker::Lorem.sentence, + uri: Faker::Internet.unique.url } ] } } - stub_rdamsc_service(true, @rdams_results.to_json) + stub_rdamsc_service(successful: true, response_body: @rdams_results.to_json) end - describe ":fetch_metadata_standards" do - it "calls :query_schemes" do + describe ':fetch_metadata_standards' do + it 'calls :query_schemes' do described_class.expects(:query_schemes).returns(nil) expect(described_class.fetch_metadata_standards).to eql(nil) end end - context "private methods" do - describe ":query_api(path:)" do - it "returns nil if path is not present" do + context 'private methods' do + describe ':query_api(path:)' do + it 'returns nil if path is not present' do expect(described_class.send(:query_api, path: nil)).to eql(nil) end - it "calls the error handler if an HTTP 200 is not received from the SPDX API" do - stub_rdamsc_service(false) + it 'calls the error handler if an HTTP 200 is not received from the SPDX API' do + stub_rdamsc_service(successful: false) described_class.expects(:handle_http_failure) expect(described_class.send(:query_api, path: Faker::Lorem.word)).to eql(nil) end - it "logs an error if the response was invalid JSON" do + it 'logs an error if the response was invalid JSON' do JSON.expects(:parse).raises(JSON::ParserError.new) described_class.expects(:log_error) expect(described_class.send(:query_api, path: Faker::Lorem.word)).to eql(nil) end - it "reuturns the array of response body as JSON" do + it 'reuturns the array of response body as JSON' do expected = JSON.parse(@rdams_results.to_json) expect(described_class.send(:query_api, path: Faker::Lorem.word)).to eql(expected) end end - describe ":query_schemes(path:)" do + describe ':query_schemes(path:)' do before(:each) do @path = Faker::Internet.unique.url end - it "returns false if the initial query returned no results" do + it 'returns false if the initial query returned no results' do described_class.expects(:query_api).with(path: @path).returns(nil) expect(described_class.send(:query_schemes, path: @path)).to eql(false) end - it "calls :process_scheme_entries if the query returned results" do + it 'calls :process_scheme_entries if the query returned results' do described_class.expects(:query_api).with(path: @path).returns(@rdams_results) described_class.expects(:process_scheme_entries) described_class.send(:query_schemes, path: @path) @@ -90,18 +89,18 @@ end end - describe ":process_scheme_entries(json:)" do - it "returns false if json is not present" do + describe ':process_scheme_entries(json:)' do + it 'returns false if json is not present' do expect(described_class.send(:process_scheme_entries, json: nil)).to eql(false) end - it "returns false if json does not contain :data not present" do - expect(described_class.send(:process_scheme_entries, json: { "foo": "bar" })).to eql(false) + it 'returns false if json does not contain :data not present' do + expect(described_class.send(:process_scheme_entries, json: { foo: 'bar' })).to eql(false) end - it "returns false if json[:data] does not contain :items present" do - json = { "data": { "items": [] } } + it 'returns false if json[:data] does not contain :items present' do + json = { data: { items: [] } } expect(described_class.send(:process_scheme_entries, json: json)).to eql(false) end - it "updates the MetadataStandard if it already exists" do + it 'updates the MetadataStandard if it already exists' do hash = @rdams_results[:data][:items].first standard = create(:metadata_standard, uri: hash[:uri], title: hash[:title]) @@ -118,7 +117,7 @@ expect(result.locations).to eql(JSON.parse(hash[:locations].to_json)) expect(result.related_entities).to eql(JSON.parse(hash[:relatedEntities].to_json)) end - it "creates a new MetadataStandard" do + it 'creates a new MetadataStandard' do hash = @rdams_results[:data][:items].first expect(described_class.send(:process_scheme_entries, @@ -133,5 +132,4 @@ end end end - end diff --git a/spec/services/external_apis/re3data_service_spec.rb b/spec/services/external_apis/re3data_service_spec.rb index eced9b4048..b56f9d55dc 100644 --- a/spec/services/external_apis/re3data_service_spec.rb +++ b/spec/services/external_apis/re3data_service_spec.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require "rails_helper" +require 'rails_helper' RSpec.describe ExternalApis::Re3dataService do - before(:each) do @repo_id = "r3d#{Faker::Number.number(digits: 6)}" @headers = described_class.headers @@ -93,228 +92,226 @@ XML - end - describe "#fetch" do - context "#fetch" do - it "returns an empty array if re3data did not return a repository list" do + describe '#fetch' do + context '#fetch' do + it 'returns an empty array if re3data did not return a repository list' do described_class.expects(:query_re3data).returns(nil) expect(described_class.fetch).to eql([]) end - it "fetches individual repository data" do + it 'fetches individual repository data' do described_class.expects(:query_re3data) - .returns(Nokogiri::XML(@repositories_results, nil, "utf8")) + .returns(Nokogiri::XML(@repositories_results, nil, 'utf8')) described_class.expects(:query_re3data_repository).at_least(1) described_class.fetch end - it "processes the repository data" do + it 'processes the repository data' do described_class.expects(:query_re3data) - .returns(Nokogiri::XML(@repositories_results, nil, "utf8")) + .returns(Nokogiri::XML(@repositories_results, nil, 'utf8')) described_class.expects(:query_re3data_repository) - .returns(Nokogiri::XML(@repository_result, nil, "utf8")) + .returns(Nokogiri::XML(@repository_result, nil, 'utf8')) described_class.expects(:process_repository).at_least(1) described_class.fetch end end end - context "private methods" do - describe "#query_re3data" do - it "calls the handle_http_failure method if a non 200 response is received" do + context 'private methods' do + describe '#query_re3data' do + it 'calls the handle_http_failure method if a non 200 response is received' do stub_request(:get, @repositories_path).with(headers: @headers) - .to_return(status: 403, body: "", headers: {}) + .to_return(status: 403, body: '', headers: {}) described_class.expects(:handle_http_failure).at_least(1) expect(described_class.send(:query_re3data)).to eql(nil) end - it "returns the response body as XML" do + it 'returns the response body as XML' do stub_request(:get, @repositories_path).with(headers: @headers) .to_return( status: 200, body: @repositories_results, headers: {} ) - expected = Nokogiri::XML(@repositories_results, nil, "utf8").text + expected = Nokogiri::XML(@repositories_results, nil, 'utf8').text expect(described_class.send(:query_re3data).text).to eql(expected) end end - describe "#query_re3data_repository(repo_id:)" do - it "returns an empty array if term is blank" do + describe '#query_re3data_repository(repo_id:)' do + it 'returns an empty array if term is blank' do expect(described_class.send(:query_re3data_repository, repo_id: nil)).to eql([]) end - it "calls the handle_http_failure method if a non 200 response is received" do + it 'calls the handle_http_failure method if a non 200 response is received' do stub_request(:get, @repository_path).with(headers: @headers) - .to_return(status: 403, body: "", headers: {}) + .to_return(status: 403, body: '', headers: {}) described_class.expects(:handle_http_failure).at_least(1) expect(described_class.send(:query_re3data_repository, repo_id: @repo_id)).to eql([]) end - it "returns the response body as JSON" do + it 'returns the response body as JSON' do stub_request(:get, @repository_path).with(headers: @headers) .to_return( status: 200, body: @repository_result, headers: {} ) - expected = Nokogiri::XML(@repository_result, nil, "utf8").text + expected = Nokogiri::XML(@repository_result, nil, 'utf8').text result = described_class.send(:query_re3data_repository, repo_id: @repo_id).text expect(result).to eql(expected) end end - describe "#process_repository(id:, node:)" do + describe '#process_repository(id:, node:)' do before(:each) do - @node = Nokogiri::XML(@repository_result, nil, "utf8") - @repo = @node.xpath("//r3d:re3data//r3d:repository").first + @node = Nokogiri::XML(@repository_result, nil, 'utf8') + @repo = @node.xpath('//r3d:re3data//r3d:repository').first end - it "returns nil if :id is not present" do + it 'returns nil if :id is not present' do expect(described_class.send(:process_repository, id: nil, node: @repo)).to eql(nil) end - it "returns nil if :node is not present" do + it 'returns nil if :node is not present' do expect(described_class.send(:process_repository, id: @repo_id, node: nil)).to eql(nil) end - it "finds an existing Repository by its identifier" do + it 'finds an existing Repository by its identifier' do repo = create(:repository, uri: @repo_id) expect(described_class.send(:process_repository, id: @repo_id, node: @repo)).to eql(repo) end - it "finds an existing Repository by its homepage" do - repo = create(:repository, homepage: @repo.xpath("//r3d:repositoryURL")&.text) + it 'finds an existing Repository by its homepage' do + repo = create(:repository, homepage: @repo.xpath('//r3d:repositoryURL')&.text) expect(described_class.send(:process_repository, id: @repo_id, node: @repo)).to eql(repo) end - it "creates a new Repository" do + it 'creates a new Repository' do repo = described_class.send(:process_repository, id: @repo_id, node: @repo) expect(repo.new_record?).to eql(false) - expect(repo.name).to eql(@repo.xpath("//r3d:repositoryName")&.text) + expect(repo.name).to eql(@repo.xpath('//r3d:repositoryName')&.text) end - it "attaches the identifier to the Repository (if it is not already defined" do + it 'attaches the identifier to the Repository (if it is not already defined' do repo = described_class.send(:process_repository, id: @repo_id, node: @repo) expect(repo.uri.ends_with?(@repo_id)).to eql(true) end end - describe "#parse_repository(repo:, node:)" do + describe '#parse_repository(repo:, node:)' do before(:each) do - doc = Nokogiri::XML(@repository_result, nil, "utf8") - @node = doc.xpath("//r3d:re3data//r3d:repository").first - @repo = create(:repository, name: @node.xpath("//r3d:repositoryName")&.text) + doc = Nokogiri::XML(@repository_result, nil, 'utf8') + @node = doc.xpath('//r3d:re3data//r3d:repository').first + @repo = create(:repository, name: @node.xpath('//r3d:repositoryName')&.text) end - it "returns nil if :repo is not present" do + it 'returns nil if :repo is not present' do expect(described_class.send(:parse_repository, repo: nil, node: @node)).to eql(nil) end - it "returns nil if :node is not present" do + it 'returns nil if :node is not present' do expect(described_class.send(:parse_repository, repo: @repo, node: nil)).to eql(nil) end - it "updates the :description" do + it 'updates the :description' do described_class.send(:parse_repository, repo: @repo, node: @node) - expect(@repo.description).to eql(@node.xpath("//r3d:description")&.text) + expect(@repo.description).to eql(@node.xpath('//r3d:description')&.text) end - it "updates the :homepage" do + it 'updates the :homepage' do described_class.send(:parse_repository, repo: @repo, node: @node) - expect(@repo.homepage).to eql(@node.xpath("//r3d:repositoryURL")&.text) + expect(@repo.homepage).to eql(@node.xpath('//r3d:repositoryURL')&.text) end - it "updates the :contact" do + it 'updates the :contact' do described_class.send(:parse_repository, repo: @repo, node: @node) - expect(@repo.contact).to eql(@node.xpath("//r3d:repositoryContact")&.text) + expect(@repo.contact).to eql(@node.xpath('//r3d:repositoryContact')&.text) end - it "updates the :info" do + it 'updates the :info' do described_class.send(:parse_repository, repo: @repo, node: @node) expect(@repo.info.present?).to eql(true) end - context ":info JSON content" do + context ':info JSON content' do before(:each) do - policies = @node.xpath("//r3d:policy").map do |node| + policies = @node.xpath('//r3d:policy').map do |node| described_class.send(:parse_policy, node: node) end - upload_types = @node.xpath("//r3d:dataUpload").map do |node| + upload_types = @node.xpath('//r3d:dataUpload').map do |node| described_class.send(:parse_upload, node: node) end @expected = { - 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, + 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: policies, upload_types: upload_types } end - it "updates the :types" do + it 'updates the :types' do described_class.send(:parse_repository, repo: @repo, node: @node) - expect(@repo.info["types"]).to eql(@expected[:types]) + expect(@repo.info['types']).to eql(@expected[:types]) end - it "updates the :subjects" do + it 'updates the :subjects' do described_class.send(:parse_repository, repo: @repo, node: @node) - expect(@repo.info["subjects"]).to eql(@expected[:subjects]) + expect(@repo.info['subjects']).to eql(@expected[:subjects]) end - it "updates the :provider_types" do + it 'updates the :provider_types' do described_class.send(:parse_repository, repo: @repo, node: @node) - expect(@repo.info["provider_types"]).to eql(@expected[:provider_types]) + expect(@repo.info['provider_types']).to eql(@expected[:provider_types]) end - it "updates the :keywords" do + it 'updates the :keywords' do described_class.send(:parse_repository, repo: @repo, node: @node) - expect(@repo.info["keywords"]).to eql(@expected[:keywords]) + expect(@repo.info['keywords']).to eql(@expected[:keywords]) end - it "updates the :access" do + it 'updates the :access' do described_class.send(:parse_repository, repo: @repo, node: @node) - expect(@repo.info["access"]).to eql(@expected[:access]) + expect(@repo.info['access']).to eql(@expected[:access]) end - it "updates the :pid_system" do + it 'updates the :pid_system' do described_class.send(:parse_repository, repo: @repo, node: @node) - expect(@repo.info["pid_system"]).to eql(@expected[:pid_system]) + expect(@repo.info['pid_system']).to eql(@expected[:pid_system]) end - it "updates the :policies" do + it 'updates the :policies' do described_class.send(:parse_repository, repo: @repo, node: @node) - expect(@repo.info["policies"].to_json).to eql(@expected[:policies].to_json) + expect(@repo.info['policies'].to_json).to eql(@expected[:policies].to_json) end - it "updates the :upload_types" do + it 'updates the :upload_types' do described_class.send(:parse_repository, repo: @repo, node: @node) - expect(@repo.info["upload_types"].to_json).to eql(@expected[:upload_types].to_json) + expect(@repo.info['upload_types'].to_json).to eql(@expected[:upload_types].to_json) end end end - describe "#parse_policy(node:)" do + describe '#parse_policy(node:)' do before(:each) do - @node = Nokogiri::XML(@repository_result, nil, "utf8") - base = @node.xpath("//r3d:re3data//r3d:repository").first + @node = Nokogiri::XML(@repository_result, nil, 'utf8') + base = @node.xpath('//r3d:re3data//r3d:repository').first @expected = { - name: base.xpath("r3d:policyName")&.text, - url: base.xpath("r3d:policyURL")&.text + name: base.xpath('r3d:policyName')&.text, + url: base.xpath('r3d:policyURL')&.text } end - it "returns nil if :node is not present" do + it 'returns nil if :node is not present' do expect(described_class.send(:parse_policy, node: nil)).to eql(nil) end - it "updates the :name" do + it 'updates the :name' do expect(described_class.send(:parse_policy, node: @node)[:name]).to eql(@expected[:name]) end - it "updates the :url" do + it 'updates the :url' do expect(described_class.send(:parse_policy, node: @node)[:url]).to eql(@expected[:url]) end end - describe "#parse_upload(node:)" do + describe '#parse_upload(node:)' do before(:each) do - @node = Nokogiri::XML(@repository_result, nil, "utf8") - base = @node.xpath("//r3d:re3data//r3d:repository").first + @node = Nokogiri::XML(@repository_result, nil, 'utf8') + base = @node.xpath('//r3d:re3data//r3d:repository').first @expected = { - type: base.xpath("r3d:dataUploadType")&.text, - restriction: base.xpath("r3d:dataUploadRestriction")&.text + type: base.xpath('r3d:dataUploadType')&.text, + restriction: base.xpath('r3d:dataUploadRestriction')&.text } end - it "returns nil if :node is not present" do + it 'returns nil if :node is not present' do expect(described_class.send(:parse_upload, node: nil)).to eql(nil) end - it "updates the :type" do + it 'updates the :type' do expect(described_class.send(:parse_upload, node: @node)[:type]).to eql(@expected[:type]) end - it "updates the :restriction" do + it 'updates the :restriction' do result = described_class.send(:parse_upload, node: @node)[:restriction] expect(result).to eql(@expected[:restriction]) end end - end end diff --git a/spec/services/external_apis/spdx_service_spec.rb b/spec/services/external_apis/spdx_service_spec.rb index 16682f281a..7cb3fa4e5b 100644 --- a/spec/services/external_apis/spdx_service_spec.rb +++ b/spec/services/external_apis/spdx_service_spec.rb @@ -1,74 +1,73 @@ # frozen_string_literal: true -require "rails_helper" +require 'rails_helper' RSpec.describe ExternalApis::SpdxService do - include Webmocks before(:each) do License.all.destroy_all @licenses_results = { - "reference": "./#{Faker::Lorem.unique.word}.html", - "isDeprecatedLicenseId": [true, false].sample, - "detailsUrl": Faker::Internet.unique.url, - "referenceNumber": Faker::Number.unique.number(digits: 2), - "name": Faker::Music::PearlJam.unique.album, - "licenseId": Faker::Music::PearlJam.unique.song.upcase.gsub(/\s/, "_"), - "seeAlso": [ + reference: "./#{Faker::Lorem.unique.word}.html", + isDeprecatedLicenseId: [true, false].sample, + detailsUrl: Faker::Internet.unique.url, + referenceNumber: Faker::Number.unique.number(digits: 2), + name: Faker::Music::PearlJam.unique.album, + licenseId: Faker::Music::PearlJam.unique.song.upcase.gsub(/\s/, '_'), + seeAlso: [ Faker::Internet.unique.url ], - "isOsiApproved": [true, false].sample + isOsiApproved: [true, false].sample } - stub_spdx_service(true, { "licenses": @licenses_results }.to_json) + stub_spdx_service(successful: true, response_body: { licenses: @licenses_results }.to_json) end - describe ":fetch" do - it "returns an empty array if spdx did not return a repository list" do + describe ':fetch' do + it 'returns an empty array if spdx did not return a repository list' do described_class.expects(:query_spdx).returns(nil) expect(described_class.fetch).to eql([]) end - it "fetches the licenses" do - described_class.expects(:query_spdx).returns({ "licenses": @licenses_results }) + it 'fetches the licenses' do + described_class.expects(:query_spdx).returns({ licenses: @licenses_results }) described_class.expects(:process_license).returns(true) described_class.fetch end end - context "private methods" do - describe ":query_spdx" do - it "calls the error handler if an HTTP 200 is not received from the SPDX API" do - stub_spdx_service(false) + context 'private methods' do + describe ':query_spdx' do + it 'calls the error handler if an HTTP 200 is not received from the SPDX API' do + stub_spdx_service(successful: false) described_class.expects(:handle_http_failure) expect(described_class.send(:query_spdx)).to eql([]) end - it "logs an error if the response was invalid JSON" do + it 'logs an error if the response was invalid JSON' do JSON.expects(:parse).raises(JSON::ParserError.new) described_class.expects(:log_error) expect(described_class.send(:query_spdx)).to eql([]) end - it "returns an empty array if the response conatins no license" do + it 'returns an empty array if the response conatins no license' do JSON.expects(:parse).returns({}) expect(described_class.send(:query_spdx)).to eql([]) end - it "reuturns the array of licenses" do + it 'reuturns the array of licenses' do expect(described_class.send(:query_spdx)).to eql(JSON.parse(@licenses_results.to_json)) end end - describe ":process_license(hash:)" do - it "returns nil if hash is empty" do + describe ':process_license(hash:)' do + it 'returns nil if hash is empty' do expect(described_class.send(:process_license, hash: nil)).to eql(nil) end - it "returns nil if a License could not be initialized" do + it 'returns nil if a License could not be initialized' do License.expects(:find_or_initialize_by).returns(nil) expect(described_class.send(:process_license, hash: @licenses_results)).to eql(nil) end - it "updates existing License records" do + it 'updates existing License records' do hash = @licenses_results license = create(:license, identifier: hash[:licenseId]) @@ -81,7 +80,7 @@ expect(result.deprecated).to eql(hash[:isDeprecatedLicenseId]) end - it "creates new License records" do + it 'creates new License records' do hash = @licenses_results expect(described_class.send(:process_license, hash: JSON.parse(hash.to_json))) @@ -94,5 +93,4 @@ end end end - end diff --git a/spec/support/helpers/webmocks.rb b/spec/support/helpers/webmocks.rb index 4a5510b740..a850e5c6c9 100644 --- a/spec/support/helpers/webmocks.rb +++ b/spec/support/helpers/webmocks.rb @@ -16,12 +16,12 @@ def stub_ror_service .to_return(status: 200, body: mocked_ror_response, headers: {}) end - def stub_spdx_service(successful = true, response_body = "") + def stub_spdx_service(successful: true, response_body: '') stub_request(:get, %r{https://raw.githubusercontent.com/spdx/.*}) .to_return(status: successful ? 200 : 500, body: response_body, headers: {}) end - def stub_rdamsc_service(successful = true, response_body = "") + def stub_rdamsc_service(successful: true, response_body: '') stub_request(:get, %r{https://rdamsc.bath.ac.uk/.*}) .to_return(status: successful ? 200 : 500, body: response_body, headers: {}) end diff --git a/spec/views/api/v1/datasets/_show.json.jbuilder_spec.rb b/spec/views/api/v1/datasets/_show.json.jbuilder_spec.rb index 2d1f465dcd..fed21bbeb0 100644 --- a/spec/views/api/v1/datasets/_show.json.jbuilder_spec.rb +++ b/spec/views/api/v1/datasets/_show.json.jbuilder_spec.rb @@ -2,33 +2,34 @@ require 'rails_helper' - context "config has disabled madmp options" do +describe 'api/v1/datasets/_show.json.jbuilder' do + context 'config has disabled madmp options' do before(:each) do @plan = create(:plan) @output = create(:research_output, plan: @plan) end - it "does not include :preservation_statement if config is false" do + it 'does not include :preservation_statement if config is false' do Rails.configuration.x.madmp.extract_preservation_statements_from_themed_questions = false - render partial: "api/v1/datasets/show", locals: { output: @output } + render partial: 'api/v1/datasets/show', locals: { output: @output } json = JSON.parse(rendered).with_indifferent_access - expect(json[:preservation_statement]).to eql("") + expect(json[:preservation_statement]).to eql('') end - it "does not include :security_and_privacy if config is false" do + it 'does not include :security_and_privacy if config is false' do Rails.configuration.x.madmp.extract_security_privacy_statements_from_themed_questions = false - render partial: "api/v1/datasets/show", locals: { output: @output } + render partial: 'api/v1/datasets/show', locals: { output: @output } json = JSON.parse(rendered).with_indifferent_access expect(json[:security_and_privacy]).to eql([]) end - it "does not include :data_quality_assurance if config is false" do + it 'does not include :data_quality_assurance if config is false' do Rails.configuration.x.madmp.extract_data_quality_statements_from_themed_questions = false - render partial: "api/v1/datasets/show", locals: { output: @output } + render partial: 'api/v1/datasets/show', locals: { output: @output } json = JSON.parse(rendered).with_indifferent_access - expect(json[:data_quality_assurance]).to eql("") + expect(json[:data_quality_assurance]).to eql('') end end - context "config has enabled madmp options" do + context 'config has enabled madmp options' do before(:each) do Rails.configuration.x.madmp.extract_preservation_statements_from_themed_questions = true Rails.configuration.x.madmp.extract_security_privacy_statements_from_themed_questions = true @@ -36,90 +37,90 @@ @plan = create(:plan) @output = create(:research_output, plan: @plan) - render partial: "api/v1/datasets/show", locals: { output: @output } + render partial: 'api/v1/datasets/show', locals: { output: @output } @json = JSON.parse(rendered).with_indifferent_access end - describe "includes all of the dataset attributes" do - it "includes :type" do + describe 'includes all of the dataset attributes' do + it 'includes :type' do expect(@json[:type]).to eql(@output.output_type) end - it "includes :title" do + it 'includes :title' do expect(@json[:title]).to eql(@output.title) end - it "includes :description" do + it 'includes :description' do expect(@json[:description]).to eql(@output.description) end - it "includes :personal_data" do + it 'includes :personal_data' do expected = Api::V1::ApiPresenter.boolean_to_yes_no_unknown(value: @output.personal_data) expect(@json[:personal_data]).to eql(expected) end - it "includes :sensitive_data" do + it 'includes :sensitive_data' do expected = Api::V1::ApiPresenter.boolean_to_yes_no_unknown(value: @output.sensitive_data) expect(@json[:sensitive_data]).to eql(expected) end - it "includes :issued" do + it 'includes :issued' do expect(@json[:issued]).to eql(@output.release_date&.to_formatted_s(:iso8601)) end - it "includes :dataset_id" do - expect(@json[:dataset_id][:type]).to eql("other") + it 'includes :dataset_id' do + expect(@json[:dataset_id][:type]).to eql('other') expect(@json[:dataset_id][:identifier]).to eql(@output.id.to_s) end - context ":distribution info" do + context ':distribution info' do before(:each) do @distribution = @json[:distribution].first end - it "includes :byte_size" do + it 'includes :byte_size' do expect(@distribution[:byte_size]).to eql(@output.byte_size) end - it "includes :data_access" do + it 'includes :data_access' do expect(@distribution[:data_access]).to eql(@output.access) end - it "includes :format" do + it 'includes :format' do expect(@distribution[:format]).to eql(nil) end end - it "includes :metadata" do + it 'includes :metadata' do expect(@json[:metadata]).not_to eql([]) expect(@json[:metadata].first[:description].present?).to eql(true) expect(@json[:metadata].first[:metadata_standard_id].present?).to eql(true) expect(@json[:metadata].first[:metadata_standard_id][:type].present?).to eql(true) expect(@json[:metadata].first[:metadata_standard_id][:identifier].present?).to eql(true) end - it "includes :technical_resources" do + it 'includes :technical_resources' do expect(@json[:technical_resources]).to eql(nil) end end - describe "includes all of the repository info as attributes" do + describe 'includes all of the repository info as attributes' do before(:each) do @host = @json[:distribution].first[:host] @expected = @output.repositories.last end - it "includes :title" do + it 'includes :title' do expect(@host[:title]).to eql(@expected.name) end - it "includes :description" do + it 'includes :description' do expect(@host[:description]).to eql(@expected.description) end - it "includes :url" do + it 'includes :url' do expect(@host[:url]).to eql(@expected.homepage) end - it "includes :dmproadmap_host_id" do - expect(@host[:dmproadmap_host_id][:type]).to eql("url") + it 'includes :dmproadmap_host_id' do + expect(@host[:dmproadmap_host_id][:type]).to eql('url') expect(@host[:dmproadmap_host_id][:identifier]).to eql(@expected.uri) end end - describe "includes all of the themed question/answers as attributes" do - it "includes :preservation_statement" do - expect(@json[:preservation_statement]).to eql("") + describe 'includes all of the themed question/answers as attributes' do + it 'includes :preservation_statement' do + expect(@json[:preservation_statement]).to eql('') end - it "includes :security_and_privacy" do + it 'includes :security_and_privacy' do expect(@json[:security_and_privacy]).to eql([]) end - it "includes :data_quality_assurance" do - expect(@json[:data_quality_assurance]).to eql("") + it 'includes :data_quality_assurance' do + expect(@json[:data_quality_assurance]).to eql('') end end end diff --git a/spec/views/layouts/modal_search/_form.html.erb_spec.rb b/spec/views/layouts/modal_search/_form.html.erb_spec.rb index 1fef4bd7ee..76b7ac25b2 100644 --- a/spec/views/layouts/modal_search/_form.html.erb_spec.rb +++ b/spec/views/layouts/modal_search/_form.html.erb_spec.rb @@ -1,15 +1,14 @@ # frozen_string_literal: true -require "rails_helper" - -describe "layouts/modal_search/_form.html.erb" do +require 'rails_helper' +describe 'layouts/modal_search/_form.html.erb' do before(:each) do @model = create(:plan) end - it "defaults to :search_examples to an empty string and :results to an empty array" do - render partial: "layouts/modal_search/form", + it 'defaults to :search_examples to an empty string and :results to an empty array' do + render partial: 'layouts/modal_search/form', locals: { namespace: nil, label: nil, @@ -18,13 +17,13 @@ search_path: nil, search_method: nil } - expect(rendered.include?("- Enter a search term -")).to eql(true) - expect(rendered.include?("No results matched your filter criteria.")).to eql(true) + expect(rendered.include?('- Enter a search term -')).to eql(true) + expect(rendered.include?('No results matched your filter criteria.')).to eql(true) end - it "uses the specified :search_examples" do + it 'uses the specified :search_examples' do examples = Faker::Lorem.sentence - render partial: "layouts/modal_search/form", + render partial: 'layouts/modal_search/form', locals: { namespace: nil, label: nil, @@ -36,9 +35,9 @@ expect(rendered.include?(examples)).to eql(true) end - it "uses the :namespace when defining the modal search sections" do + it 'uses the :namespace when defining the modal search sections' do namespace = Faker::Lorem.word.downcase - render partial: "layouts/modal_search/form", + render partial: 'layouts/modal_search/form', locals: { namespace: namespace, label: nil, @@ -52,9 +51,9 @@ expect(rendered.include?("modal-search-#{namespace}-results")).to eql(true) end - it "Uses the :label for the button" do + it 'Uses the :label for the button' do label = Faker::Lorem.word - render partial: "layouts/modal_search/form", + render partial: 'layouts/modal_search/form', locals: { namespace: nil, label: label, @@ -66,8 +65,8 @@ expect(rendered.include?("#{label} search")).to eql(true) end - it "Uses the :model_instance when adding the form element" do - render partial: "layouts/modal_search/form", + it 'Uses the :model_instance when adding the form element' do + render partial: 'layouts/modal_search/form', locals: { namespace: nil, label: nil, @@ -79,9 +78,9 @@ expect(rendered.include?(plan_path(@model))).to eql(true) end - it "Uses the :search_path when adding the form element" do + it 'Uses the :search_path when adding the form element' do url = Faker::Internet.url - render partial: "layouts/modal_search/form", + render partial: 'layouts/modal_search/form', locals: { namespace: nil, label: nil, @@ -93,9 +92,9 @@ expect(rendered.include?(url)).to eql(true) end - it "Uses the :search_method when adding the form element" do + it 'Uses the :search_method when adding the form element' do method = %i[get put post patch delete].sample - render partial: "layouts/modal_search/form", + render partial: 'layouts/modal_search/form', locals: { namespace: nil, label: nil, diff --git a/spec/views/layouts/modal_search/_result.html.erb_spec.rb b/spec/views/layouts/modal_search/_result.html.erb_spec.rb index 92ee4b953d..6266872334 100644 --- a/spec/views/layouts/modal_search/_result.html.erb_spec.rb +++ b/spec/views/layouts/modal_search/_result.html.erb_spec.rb @@ -1,28 +1,27 @@ # frozen_string_literal: true -require "rails_helper" - -describe "layouts/modal_search/_result.html.erb" do +require 'rails_helper' +describe 'layouts/modal_search/_result.html.erb' do before(:each) do @result = build(:repository) end - it "renders the :result_partial if specified" do - render partial: "layouts/modal_search/result", + it 'renders the :result_partial if specified' do + render partial: 'layouts/modal_search/result', locals: { item_name_attr: :name, result: @result, selected: nil, - result_partial: "layouts/footer", + result_partial: 'layouts/footer', search_path: nil, search_method: nil } - expect(response).to render_template(partial: "layouts/_footer") + expect(response).to render_template(partial: 'layouts/_footer') end - it "does not render the :result_partial if none is specified" do - render partial: "layouts/modal_search/result", + it 'does not render the :result_partial if none is specified' do + render partial: 'layouts/modal_search/result', locals: { item_name_attr: :name, result: @result, @@ -31,11 +30,11 @@ search_path: nil, search_method: nil } - expect(response).not_to render_template(partial: "layouts/footer") + expect(response).not_to render_template(partial: 'layouts/footer') end it "displays the result's :item_name_attr" do - render partial: "layouts/modal_search/result", + render partial: 'layouts/modal_search/result', locals: { item_name_attr: :name, result: @result, @@ -44,12 +43,12 @@ search_path: nil, search_method: nil } - expect(rendered.include?("modal-search-result-selector hidden")).to eql(true) - expect(rendered.include?("modal-search-result-unselector hidden")).to eql(false) + expect(rendered.include?('modal-search-result-selector hidden')).to eql(true) + expect(rendered.include?('modal-search-result-unselector hidden')).to eql(false) end it "hides the 'Select' button and shows the 'Remove' button when :selected is true" do - render partial: "layouts/modal_search/result", + render partial: 'layouts/modal_search/result', locals: { item_name_attr: :name, result: @result, @@ -58,12 +57,12 @@ search_path: nil, search_method: nil } - expect(rendered.include?("modal-search-result-selector hidden")).to eql(true) - expect(rendered.include?("modal-search-result-unselector hidden")).to eql(false) + expect(rendered.include?('modal-search-result-selector hidden')).to eql(true) + expect(rendered.include?('modal-search-result-unselector hidden')).to eql(false) end it "shows the 'Select' button and hides the 'Remove' button when :selected is false" do - render partial: "layouts/modal_search/result", + render partial: 'layouts/modal_search/result', locals: { item_name_attr: :name, result: @result, @@ -72,8 +71,7 @@ search_path: nil, search_method: nil } - expect(rendered.include?("modal-search-result-selector hidden")).to eql(false) - expect(rendered.include?("modal-search-result-unselector hidden")).to eql(true) + expect(rendered.include?('modal-search-result-selector hidden')).to eql(false) + expect(rendered.include?('modal-search-result-unselector hidden')).to eql(true) end - end diff --git a/spec/views/layouts/modal_search/_results.html.erb_spec.rb b/spec/views/layouts/modal_search/_results.html.erb_spec.rb index 1a5bffccd8..13fbea9d50 100644 --- a/spec/views/layouts/modal_search/_results.html.erb_spec.rb +++ b/spec/views/layouts/modal_search/_results.html.erb_spec.rb @@ -1,16 +1,15 @@ # frozen_string_literal: true -require "rails_helper" - -describe "layouts/modal_search/_selections.html.erb" do +require 'rails_helper' +describe 'layouts/modal_search/_selections.html.erb' do before(:each) do create(:repository) - @msg = "No results matched your filter criteria." + @msg = 'No results matched your filter criteria.' end - it "defaults :results to an empty array, :selected to false, and has a default :no_results_msg" do - render partial: "layouts/modal_search/results", + it 'defaults :results to an empty array, :selected to false, and has a default :no_results_msg' do + render partial: 'layouts/modal_search/results', locals: { namespace: nil, item_name_attr: nil, @@ -21,9 +20,9 @@ expect(rendered.include?(@msg)).to eql(true) end - context "when :selected is false" do - it "displays pagination when :results is not empty and does not display no results message" do - render partial: "layouts/modal_search/results", + context 'when :selected is false' do + it 'displays pagination when :results is not empty and does not display no results message' do + render partial: 'layouts/modal_search/results', locals: { namespace: nil, results: Repository.all.page(1), @@ -33,11 +32,11 @@ search_path: nil, search_method: nil } - expect(rendered.include?("modal-search-results-pagination")).to eql(true) + expect(rendered.include?('modal-search-results-pagination')).to eql(true) expect(rendered.include?(@msg)).to eql(false) end - it "does not display pagination when :results is empty and displays the message" do - render partial: "layouts/modal_search/results", + it 'does not display pagination when :results is empty and displays the message' do + render partial: 'layouts/modal_search/results', locals: { namespace: nil, results: [], @@ -47,14 +46,14 @@ search_path: nil, search_method: nil } - expect(rendered.include?("modal-search-results-pagination")).to eql(false) + expect(rendered.include?('modal-search-results-pagination')).to eql(false) expect(rendered.include?(@msg)).to eql(true) end end - context "when :selected is true" do - it "does not display pagination when :results is not empty and does not display message" do - render partial: "layouts/modal_search/results", + context 'when :selected is true' do + it 'does not display pagination when :results is not empty and does not display message' do + render partial: 'layouts/modal_search/results', locals: { namespace: nil, results: Repository.all.page(1), @@ -64,11 +63,11 @@ search_path: nil, search_method: nil } - expect(rendered.include?("modal-search-results-pagination")).to eql(false) + expect(rendered.include?('modal-search-results-pagination')).to eql(false) expect(rendered.include?(@msg)).to eql(false) end - it "does not display pagination when :results is empty and does not display message" do - render partial: "layouts/modal_search/results", + it 'does not display pagination when :results is empty and does not display message' do + render partial: 'layouts/modal_search/results', locals: { namespace: nil, results: [], @@ -78,9 +77,8 @@ search_path: nil, search_method: nil } - expect(rendered.include?("modal-search-results-pagination")).to eql(false) + expect(rendered.include?('modal-search-results-pagination')).to eql(false) expect(rendered.include?(@msg)).to eql(false) end end - end diff --git a/spec/views/layouts/modal_search/_selections.html.erb_spec.rb b/spec/views/layouts/modal_search/_selections.html.erb_spec.rb index 05f4299b68..46ea4ba805 100644 --- a/spec/views/layouts/modal_search/_selections.html.erb_spec.rb +++ b/spec/views/layouts/modal_search/_selections.html.erb_spec.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true -require "rails_helper" - -describe "layouts/modal_search/_selections.html.erb" do +require 'rails_helper' +describe 'layouts/modal_search/_selections.html.erb' do before(:each) do @namespace = Faker::Lorem.word.downcase @label = Faker::Lorem.sentence - render partial: "layouts/modal_search/selections", + render partial: 'layouts/modal_search/selections', locals: { namespace: @namespace, button_label: @label, @@ -19,20 +18,19 @@ } end - it "adds the :namespace to the selections block" do + it 'adds the :namespace to the selections block' do expect(rendered.include?("modal-search-#{@namespace}-selections")).to eql(true) end - it "adds the :namespace to the button" do + it 'adds the :namespace to the button' do expect(rendered.include?("target=\"#modal-search-#{@namespace}\"")).to eql(true) end - it "sets the :button_label on the button" do + it 'sets the :button_label on the button' do expect(rendered.include?(@label)).to eql(true) end - it "adds the renders the results partial" do - expect(response).to render_template(partial: "layouts/modal_search/_results") + it 'adds the renders the results partial' do + expect(response).to render_template(partial: 'layouts/modal_search/_results') end - end From 4aa76f8a715f642bff3f1f884a5a50ec8b9c3201 Mon Sep 17 00:00:00 2001 From: briri Date: Wed, 2 Feb 2022 09:33:51 -0800 Subject: [PATCH 10/59] revert change to plans factory --- spec/factories/plans.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spec/factories/plans.rb b/spec/factories/plans.rb index 04fc6ea052..6361c98bc1 100644 --- a/spec/factories/plans.rb +++ b/spec/factories/plans.rb @@ -62,9 +62,7 @@ end trait :creator do after(:create) do |obj| - owner = create(:user, org: create(:org)) - obj.roles << create(:role, :creator, user: owner) - obj.update(org: owner.org) + obj.roles << create(:role, :creator, user: create(:user, org: create(:org))) end end trait :commenter do From 699e724c0bfae8189b1a23db937ffcfa39a0b0ed Mon Sep 17 00:00:00 2001 From: briri Date: Fri, 25 Feb 2022 08:06:47 -0800 Subject: [PATCH 11/59] fix issue with display of org links and email --- app/views/layouts/_branding.html.erb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/views/layouts/_branding.html.erb b/app/views/layouts/_branding.html.erb index 5ce0a8fd7d..cbcaf31595 100644 --- a/app/views/layouts/_branding.html.erb +++ b/app/views/layouts/_branding.html.erb @@ -23,10 +23,14 @@
    - <%= 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" %>
    From 2392cf85c3bfd53824f941e713de55679afc38de Mon Sep 17 00:00:00 2001 From: Nicolas Franck Date: Mon, 7 Mar 2022 14:16:32 +0100 Subject: [PATCH 18/59] link to active role to deactivate. See issue 3121 --- app/views/paginable/plans/_privately_visible.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/paginable/plans/_privately_visible.html.erb b/app/views/paginable/plans/_privately_visible.html.erb index 59d7858e27..427b1da26a 100644 --- a/app/views/paginable/plans/_privately_visible.html.erb +++ b/app/views/paginable/plans/_privately_visible.html.erb @@ -23,7 +23,7 @@ <%= plan.template.title %> <%= l(plan.updated_at.to_date, formats: :short) %> - <%= display_role(plan.roles.select { |r| r.user_id == current_user.id }.first) %> + <%= display_role(plan.roles.select(&:active).select { |r| r.user_id == current_user.id }.first) %> <% if plan.administerable_by?(current_user.id) then %> <%= check_box_tag :is_test, "1", (plan.visibility === "is_test"), From e466fb45cf3c08991258a1e000317dc97fd717d6 Mon Sep 17 00:00:00 2001 From: Nicolas Franck Date: Mon, 7 Mar 2022 14:20:55 +0100 Subject: [PATCH 19/59] missed some other stuff --- app/views/paginable/plans/_privately_visible.html.erb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/paginable/plans/_privately_visible.html.erb b/app/views/paginable/plans/_privately_visible.html.erb index 427b1da26a..cc1ad4d421 100644 --- a/app/views/paginable/plans/_privately_visible.html.erb +++ b/app/views/paginable/plans/_privately_visible.html.erb @@ -16,6 +16,7 @@ <% scope.each do |plan| %> + <% my_plan_roles = plan.roles.select(&:active).select { |r| r.user_id == current_user.id } %> <%= link_to "#{plan.title.length > 60 ? "#{plan.title[0..59]} ..." : plan.title}", @@ -23,7 +24,7 @@ <%= plan.template.title %> <%= l(plan.updated_at.to_date, formats: :short) %> - <%= display_role(plan.roles.select(&:active).select { |r| r.user_id == current_user.id }.first) %> + <%= display_role(my_plan_roles.first) %> <% if plan.administerable_by?(current_user.id) then %> <%= check_box_tag :is_test, "1", (plan.visibility === "is_test"), @@ -69,7 +70,7 @@
  • <%= link_to _('Download'), download_plan_path(plan) %>
  • <% end %> <% end %> - <% role = plan.roles.select { |r| r.user_id == current_user.id }.first %> + <% role = my_plan_roles.first %> <% conf = (role.creator? && plan.publicly_visible?) ? _("Are you sure you wish to remove this public plan? This will remove it from the Public DMPs page but any collaborators will still be able to access it.") : _("Are you sure you wish to remove this plan? Any collaborators will still be able to access it.") %>
  • <%= link_to _('Remove'), deactivate_role_path(role), 'data-method': 'put', rel: 'nofollow', 'data-confirm': conf %>
  • From bb5941e5d8998ba142be014c35bdb964f4fd86c5 Mon Sep 17 00:00:00 2001 From: Benjamin FAURE Date: Tue, 8 Mar 2022 10:56:20 +0100 Subject: [PATCH 20/59] Added toggleable guidance/comments section --- .../stylesheets/blocks/_question_form.scss | 35 +++++++++++++++++++ app/javascript/src/answers/edit.js | 9 +++++ app/views/phases/_edit_plan_answers.html.erb | 16 +++++++-- config/initializers/_dmproadmap.rb | 4 +++ 4 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 app/assets/stylesheets/blocks/_question_form.scss diff --git a/app/assets/stylesheets/blocks/_question_form.scss b/app/assets/stylesheets/blocks/_question_form.scss new file mode 100644 index 0000000000..301e5861e4 --- /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-grey-darker; + 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-grey-darker; + padding-left: 5px; + } + +} \ No newline at end of file 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/views/phases/_edit_plan_answers.html.erb b/app/views/phases/_edit_plan_answers.html.erb index ae13124610..9c4467840c 100644 --- a/app/views/phases/_edit_plan_answers.html.erb +++ b/app/views/phases/_edit_plan_answers.html.erb @@ -65,9 +65,18 @@ answer = Answer.new({ plan: plan, question: question }) end %> -
    -
    +
    +
    + <% + guidance_comments_opened = Rails.configuration.x.application.guidance_comments_opened + %> + <% if Rails.configuration.x.application.guidance_comments_toggleable %> +
    + + <%= _('Comments & Guidance') %> +
    + <% end %>
    " class="answer-locking"> @@ -87,7 +96,8 @@
    -
    + <% style = guidance_comments_opened ? '' : 'display: none' %> +
    <%= render partial: '/phases/guidances_notes', locals: { plan: plan, diff --git a/config/initializers/_dmproadmap.rb b/config/initializers/_dmproadmap.rb index 06c664419a..fa04bf6f9e 100644 --- a/config/initializers/_dmproadmap.rb +++ b/config/initializers/_dmproadmap.rb @@ -122,6 +122,10 @@ class Application < Rails::Application config.x.application.require_contributor_name = false config.x.application.require_contributor_email = false + # Defines if Guidances/Comments in toggleable & if it's opened by default + config.x.application.guidance_comments_toggleable = true + config.x.application.guidance_comments_opened = true + # ------------------- # # SHIBBOLETH SETTINGS # # ------------------- # From 1c2b0429209fb971f8d3527f9c24f5dc253815df Mon Sep 17 00:00:00 2001 From: Benjamin FAURE Date: Wed, 9 Mar 2022 08:03:24 +0100 Subject: [PATCH 21/59] Changed comments section config variable name --- app/views/phases/_edit_plan_answers.html.erb | 6 +++--- config/initializers/_dmproadmap.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/phases/_edit_plan_answers.html.erb b/app/views/phases/_edit_plan_answers.html.erb index 9c4467840c..3d41817a05 100644 --- a/app/views/phases/_edit_plan_answers.html.erb +++ b/app/views/phases/_edit_plan_answers.html.erb @@ -69,11 +69,11 @@
    <% - guidance_comments_opened = Rails.configuration.x.application.guidance_comments_opened + 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 %> @@ -96,7 +96,7 @@
    - <% style = guidance_comments_opened ? '' : 'display: none' %> + <% style = guidance_comments_opened_by_default ? '' : 'display: none' %>
    <%= render partial: '/phases/guidances_notes', locals: { diff --git a/config/initializers/_dmproadmap.rb b/config/initializers/_dmproadmap.rb index fa04bf6f9e..82cabfe3cb 100644 --- a/config/initializers/_dmproadmap.rb +++ b/config/initializers/_dmproadmap.rb @@ -124,7 +124,7 @@ class Application < Rails::Application # Defines if Guidances/Comments in toggleable & if it's opened by default config.x.application.guidance_comments_toggleable = true - config.x.application.guidance_comments_opened = true + config.x.application.guidance_comments_opened_by_default = true # ------------------- # # SHIBBOLETH SETTINGS # From 8e95a5b050e24dd3181429bedf1c7afa649e7a23 Mon Sep 17 00:00:00 2001 From: Benjamin FAURE Date: Wed, 9 Mar 2022 08:04:28 +0100 Subject: [PATCH 22/59] Changed fontawesome chevron icon --- app/views/phases/_edit_plan_answers.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/phases/_edit_plan_answers.html.erb b/app/views/phases/_edit_plan_answers.html.erb index 3d41817a05..df53921eb3 100644 --- a/app/views/phases/_edit_plan_answers.html.erb +++ b/app/views/phases/_edit_plan_answers.html.erb @@ -73,7 +73,7 @@ %> <% if Rails.configuration.x.application.guidance_comments_toggleable %>
    - + <%= _('Comments & Guidance') %>
    <% end %> From 21797d18e71b2544e067f6ea2e6eea5e33af4497 Mon Sep 17 00:00:00 2001 From: Ray Carrick Date: Thu, 10 Mar 2022 13:49:18 +0000 Subject: [PATCH 23/59] translation sync 2022-03-10 --- config/locale/app.pot | 47 +- config/locale/de/LC_MESSAGES/app.mo | Bin 146799 -> 146724 bytes config/locale/de/app.po | 49 +- config/locale/en_CA/LC_MESSAGES/app.mo | Bin 630 -> 630 bytes config/locale/en_CA/app.po | 47 +- config/locale/en_GB/LC_MESSAGES/app.mo | Bin 1916 -> 1916 bytes config/locale/en_GB/app.po | 47 +- config/locale/en_US/LC_MESSAGES/app.mo | Bin 20973 -> 20973 bytes config/locale/en_US/app.po | 47 +- config/locale/es/LC_MESSAGES/app.mo | Bin 143576 -> 143503 bytes config/locale/es/app.po | 49 +- config/locale/fi/LC_MESSAGES/app.mo | Bin 138723 -> 138638 bytes config/locale/fi/app.po | 57 +- config/locale/fr_CA/LC_MESSAGES/app.mo | Bin 122826 -> 122904 bytes config/locale/fr_CA/app.po | 49 +- config/locale/fr_FR/LC_MESSAGES/app.mo | Bin 146804 -> 146235 bytes config/locale/fr_FR/app.po | 780 +++++++++++++------------ config/locale/pt_BR/LC_MESSAGES/app.mo | Bin 142200 -> 142139 bytes config/locale/pt_BR/app.po | 85 ++- config/locale/sv_FI/LC_MESSAGES/app.mo | Bin 138591 -> 138520 bytes config/locale/sv_FI/app.po | 49 +- config/locale/tr_TR/LC_MESSAGES/app.mo | Bin 125610 -> 125545 bytes config/locale/tr_TR/app.po | 47 +- config/locales/.translation_io | 2 +- config/locales/translation.de.yml | 9 - config/locales/translation.en-CA.yml | 3 - config/locales/translation.en-GB.yml | 3 - config/locales/translation.en-US.yml | 3 - config/locales/translation.es.yml | 10 - config/locales/translation.fi.yml | 7 - config/locales/translation.fr-CA.yml | 11 - config/locales/translation.fr-FR.yml | 33 +- config/locales/translation.pt-BR.yml | 12 - config/locales/translation.sv-FI.yml | 10 - config/locales/translation.tr-TR.yml | 8 - 35 files changed, 666 insertions(+), 798 deletions(-) diff --git a/config/locale/app.pot b/config/locale/app.pot index 8fbf5ca3ff..4f51ed0d43 100644 --- a/config/locale/app.pot +++ b/config/locale/app.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: app 1.0\n" "Report-Msgid-Bugs-To: contact@translation.io\n" -"POT-Creation-Date: 2022-02-10 08:50+0000\n" -"PO-Revision-Date: 2022-02-10 08:50+0000\n" +"POT-Creation-Date: 2022-03-10 13:47+0000\n" +"PO-Revision-Date: 2022-03-10 13:47+0000\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" @@ -36,11 +36,11 @@ msgstr "" msgid "Departments code and name must be unique" msgstr "" -#: ../../app/controllers/api/v0/plans_controller.rb:27 +#: ../../app/controllers/api/v0/plans_controller.rb:28 msgid "user must be in your organisation" msgstr "" -#: ../../app/controllers/api/v0/plans_controller.rb:56 +#: ../../app/controllers/api/v0/plans_controller.rb:57 msgid "Bad Parameters" msgstr "" @@ -204,8 +204,8 @@ msgstr "" msgid "Restricted access to View All the records" msgstr "" -#: ../../app/controllers/concerns/paginable.rb:182 -msgid "Sort by %s" +#: ../../app/controllers/concerns/paginable.rb:183 +msgid "Sort by %" msgstr "" #: ../../app/controllers/concerns/template_methods.rb:8 @@ -1830,9 +1830,9 @@ msgid "" msgstr "" #: ../../app/views/contact_us/contacts/new.html.erb:10 -#: ../../app/views/layouts/_branding.html.erb:37 -#: ../../app/views/layouts/_footer.html.erb:24 -#: ../../app/views/layouts/_footer.html.erb:30 +#: ../../app/views/layouts/_branding.html.erb:41 +#: ../../app/views/layouts/_footer.html.erb:23 +#: ../../app/views/layouts/_footer.html.erb:29 #: ../../app/views/org_admin/plans/index.html.erb:40 #: ../../app/views/paginable/plans/_organisationally_or_publicly_visible.html.erb:39 #: ../../app/views/paginable/plans/_publicly_visible.html.erb:24 @@ -2324,7 +2324,6 @@ msgid "Do you have a %{application_name} account?" msgstr "" #: ../../app/views/devise/registrations/new.html.erb:22 -#: ../../app/views/layouts/_navigation.html.erb:63 #: ../../app/views/layouts/_signin_signout.html.erb:41 #: ../../app/views/shared/_access_controls.html.erb:5 #: ../../app/views/shared/_sign_in_form.html.erb:19 @@ -2411,7 +2410,7 @@ msgstr "" #: ../../app/views/guidances/admin_index.html.erb:5 #: ../../app/views/guidances/new_edit.html.erb:12 #: ../../app/views/guidances/new_edit.html.erb:16 -#: ../../app/views/layouts/_branding.html.erb:77 +#: ../../app/views/layouts/_branding.html.erb:82 #: ../../app/views/org_admin/annotations/_form.html.erb:3 #: ../../app/views/org_admin/questions/_show.html.erb:78 #: ../../app/views/paginable/themes/_index.html.erb:5 @@ -2516,40 +2515,40 @@ msgstr "" msgid "Previous" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:60 +#: ../../app/views/layouts/_branding.html.erb:65 msgid "Admin" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:66 +#: ../../app/views/layouts/_branding.html.erb:71 #: ../../app/views/org_admin/users/edit.html.erb:71 #: ../../app/views/paginable/users/_index.html.erb:26 #: ../../app/views/super_admin/users/edit.html.erb:105 msgid "Plans" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:72 +#: ../../app/views/layouts/_branding.html.erb:77 #: ../../app/views/org_admin/templates/index.html.erb:4 #: ../../app/views/paginable/orgs/_index.html.erb:8 msgid "Templates" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:82 +#: ../../app/views/layouts/_branding.html.erb:87 #: ../../app/views/super_admin/orgs/index.html.erb:2 #: ../../app/views/super_admin/orgs/index.html.erb:6 msgid "Organisations" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:87 +#: ../../app/views/layouts/_branding.html.erb:92 #: ../../app/views/orgs/admin_edit.html.erb:1 #: ../../app/views/orgs/admin_edit.html.erb:6 msgid "Organisation details" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:93 +#: ../../app/views/layouts/_branding.html.erb:98 msgid "Users" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:98 +#: ../../app/views/layouts/_branding.html.erb:103 #: ../../app/views/org_admin/questions/_show.html.erb:88 #: ../../app/views/org_admin/shared/_theme_selector.html.erb:16 #: ../../app/views/paginable/guidances/_index.html.erb:7 @@ -2558,15 +2557,15 @@ msgstr "" msgid "Themes" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:103 +#: ../../app/views/layouts/_branding.html.erb:108 msgid "Usage" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:108 +#: ../../app/views/layouts/_branding.html.erb:113 msgid "Api Clients" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:111 +#: ../../app/views/layouts/_branding.html.erb:116 #: ../../app/views/org_admin/plans/index.html.erb:11 #: ../../app/views/super_admin/notifications/index.html.erb:1 #: ../../app/views/super_admin/notifications/index.html.erb:4 @@ -2594,11 +2593,7 @@ msgid "Privacy statement" msgstr "" #: ../../app/views/layouts/_footer.html.erb:17 -msgid "Accessibility statement" -msgstr "" - -#: ../../app/views/layouts/_footer.html.erb:18 -msgid "Github" +msgid "GitHub" msgstr "" #: ../../app/views/layouts/_navigation.html.erb:12 diff --git a/config/locale/de/LC_MESSAGES/app.mo b/config/locale/de/LC_MESSAGES/app.mo index 026e9cd0bf441932e16c2f41dd28c1841850747e..3112b48a6794490c03ac965875927fc17f546495 100644 GIT binary patch delta 24346 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 Xc5c(9&5l>|JY6d8Xnxm|q{;sQka=Lt delta 24408 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\n" "Language-Team: German\n" "Language: de\n" @@ -40,11 +40,11 @@ msgstr "Schlechte Referenzen" msgid "Departments code and name must be unique" msgstr "Code und Name der Abteilungen müssen eindeutig sein" -#: ../../app/controllers/api/v0/plans_controller.rb:27 +#: ../../app/controllers/api/v0/plans_controller.rb:28 msgid "user must be in your organisation" msgstr "der/die Nutzer/in muss in Ihrer Organisation sein" -#: ../../app/controllers/api/v0/plans_controller.rb:56 +#: ../../app/controllers/api/v0/plans_controller.rb:57 msgid "Bad Parameters" msgstr "Schlechte Parameter" @@ -210,9 +210,9 @@ msgstr "Einheimische sollten ein Hash-Objekt sein" msgid "Restricted access to View All the records" msgstr "Eingeschränkter Zugriff auf Alle Datensätze anzeigen" -#: ../../app/controllers/concerns/paginable.rb:182 -msgid "Sort by %s" -msgstr "Sortiere nach % S" +#: ../../app/controllers/concerns/paginable.rb:183 +msgid "Sort by %" +msgstr "Sortiere nach %" #: ../../app/controllers/concerns/template_methods.rb:8 msgid "customisation" @@ -1929,9 +1929,9 @@ msgstr "" "itte das folgende Formular aus." #: ../../app/views/contact_us/contacts/new.html.erb:10 -#: ../../app/views/layouts/_branding.html.erb:37 -#: ../../app/views/layouts/_footer.html.erb:24 -#: ../../app/views/layouts/_footer.html.erb:30 +#: ../../app/views/layouts/_branding.html.erb:41 +#: ../../app/views/layouts/_footer.html.erb:23 +#: ../../app/views/layouts/_footer.html.erb:29 #: ../../app/views/org_admin/plans/index.html.erb:40 #: ../../app/views/paginable/plans/_organisationally_or_publicly_visible.html.erb:39 #: ../../app/views/paginable/plans/_publicly_visible.html.erb:24 @@ -2470,7 +2470,6 @@ msgid "Do you have a %{application_name} account?" msgstr "Hast du einen %{application_name}-Account?" #: ../../app/views/devise/registrations/new.html.erb:22 -#: ../../app/views/layouts/_navigation.html.erb:63 #: ../../app/views/layouts/_signin_signout.html.erb:41 #: ../../app/views/shared/_access_controls.html.erb:5 #: ../../app/views/shared/_sign_in_form.html.erb:19 @@ -2567,7 +2566,7 @@ msgstr "Alle Hilfestellungen ansehen" #: ../../app/views/guidances/admin_index.html.erb:5 #: ../../app/views/guidances/new_edit.html.erb:12 #: ../../app/views/guidances/new_edit.html.erb:16 -#: ../../app/views/layouts/_branding.html.erb:77 +#: ../../app/views/layouts/_branding.html.erb:82 #: ../../app/views/org_admin/annotations/_form.html.erb:3 #: ../../app/views/org_admin/questions/_show.html.erb:78 #: ../../app/views/paginable/themes/_index.html.erb:5 @@ -2696,40 +2695,40 @@ msgstr "Nächster" msgid "Previous" msgstr "Früher" -#: ../../app/views/layouts/_branding.html.erb:60 +#: ../../app/views/layouts/_branding.html.erb:65 msgid "Admin" msgstr "Administrator" -#: ../../app/views/layouts/_branding.html.erb:66 +#: ../../app/views/layouts/_branding.html.erb:71 #: ../../app/views/org_admin/users/edit.html.erb:71 #: ../../app/views/paginable/users/_index.html.erb:26 #: ../../app/views/super_admin/users/edit.html.erb:105 msgid "Plans" msgstr "Pläne" -#: ../../app/views/layouts/_branding.html.erb:72 +#: ../../app/views/layouts/_branding.html.erb:77 #: ../../app/views/org_admin/templates/index.html.erb:4 #: ../../app/views/paginable/orgs/_index.html.erb:8 msgid "Templates" msgstr "Vorlagen" -#: ../../app/views/layouts/_branding.html.erb:82 +#: ../../app/views/layouts/_branding.html.erb:87 #: ../../app/views/super_admin/orgs/index.html.erb:2 #: ../../app/views/super_admin/orgs/index.html.erb:6 msgid "Organisations" msgstr "Organisationen" -#: ../../app/views/layouts/_branding.html.erb:87 +#: ../../app/views/layouts/_branding.html.erb:92 #: ../../app/views/orgs/admin_edit.html.erb:1 #: ../../app/views/orgs/admin_edit.html.erb:6 msgid "Organisation details" msgstr "Organisations-Details" -#: ../../app/views/layouts/_branding.html.erb:93 +#: ../../app/views/layouts/_branding.html.erb:98 msgid "Users" msgstr "Nutzer" -#: ../../app/views/layouts/_branding.html.erb:98 +#: ../../app/views/layouts/_branding.html.erb:103 #: ../../app/views/org_admin/questions/_show.html.erb:88 #: ../../app/views/org_admin/shared/_theme_selector.html.erb:16 #: ../../app/views/paginable/guidances/_index.html.erb:7 @@ -2738,15 +2737,15 @@ msgstr "Nutzer" msgid "Themes" msgstr "Themen" -#: ../../app/views/layouts/_branding.html.erb:103 +#: ../../app/views/layouts/_branding.html.erb:108 msgid "Usage" msgstr "Usage" -#: ../../app/views/layouts/_branding.html.erb:108 +#: ../../app/views/layouts/_branding.html.erb:113 msgid "Api Clients" msgstr "API-Clients" -#: ../../app/views/layouts/_branding.html.erb:111 +#: ../../app/views/layouts/_branding.html.erb:116 #: ../../app/views/org_admin/plans/index.html.erb:11 #: ../../app/views/super_admin/notifications/index.html.erb:1 #: ../../app/views/super_admin/notifications/index.html.erb:4 @@ -2774,11 +2773,7 @@ msgid "Privacy statement" msgstr "Datenschutzerklärung" #: ../../app/views/layouts/_footer.html.erb:17 -msgid "Accessibility statement" -msgstr "Erklärung zur Barrierefreiheit" - -#: ../../app/views/layouts/_footer.html.erb:18 -msgid "Github" +msgid "GitHub" msgstr "Github" #: ../../app/views/layouts/_navigation.html.erb:12 diff --git a/config/locale/en_CA/LC_MESSAGES/app.mo b/config/locale/en_CA/LC_MESSAGES/app.mo index 0f011e8dbc47054e9bbb64ff386fecb31f2c5432..ed0daebaa32da2fdaf2a38dc18bfdc3daf1faf26 100644 GIT binary patch delta 38 ocmeyy@{MJK5+j$fuAzZ~p|O>T`D7hNIVjh}%EV%G2%{1s0Ki-by8r+H delta 38 ncmeyy@{MJK5+j$9uAzZ~frXW+!DJmqIVjf>$lV;msKf{WzzYbp diff --git a/config/locale/en_CA/app.po b/config/locale/en_CA/app.po index 0d16b746b4..214aeca7d7 100644 --- a/config/locale/en_CA/app.po +++ b/config/locale/en_CA/app.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: app 1.0\n" "Report-Msgid-Bugs-To: contact@translation.io\n" -"POT-Creation-Date: 2022-02-10 08:50+0000\n" -"PO-Revision-Date: 2022-02-10 09:50+0100\n" +"POT-Creation-Date: 2022-03-10 13:47+0000\n" +"PO-Revision-Date: 2022-03-10 14:48+0100\n" "Last-Translator: FULL NAME \n" "Language-Team: English\n" "Language: en_CA\n" @@ -36,11 +36,11 @@ msgstr "" msgid "Departments code and name must be unique" msgstr "" -#: ../../app/controllers/api/v0/plans_controller.rb:27 +#: ../../app/controllers/api/v0/plans_controller.rb:28 msgid "user must be in your organisation" msgstr "" -#: ../../app/controllers/api/v0/plans_controller.rb:56 +#: ../../app/controllers/api/v0/plans_controller.rb:57 msgid "Bad Parameters" msgstr "" @@ -204,8 +204,8 @@ msgstr "" msgid "Restricted access to View All the records" msgstr "" -#: ../../app/controllers/concerns/paginable.rb:182 -msgid "Sort by %s" +#: ../../app/controllers/concerns/paginable.rb:183 +msgid "Sort by %" msgstr "" #: ../../app/controllers/concerns/template_methods.rb:8 @@ -1831,9 +1831,9 @@ msgid "" msgstr "" #: ../../app/views/contact_us/contacts/new.html.erb:10 -#: ../../app/views/layouts/_branding.html.erb:37 -#: ../../app/views/layouts/_footer.html.erb:24 -#: ../../app/views/layouts/_footer.html.erb:30 +#: ../../app/views/layouts/_branding.html.erb:41 +#: ../../app/views/layouts/_footer.html.erb:23 +#: ../../app/views/layouts/_footer.html.erb:29 #: ../../app/views/org_admin/plans/index.html.erb:40 #: ../../app/views/paginable/plans/_organisationally_or_publicly_visible.html.erb:39 #: ../../app/views/paginable/plans/_publicly_visible.html.erb:24 @@ -2325,7 +2325,6 @@ msgid "Do you have a %{application_name} account?" msgstr "" #: ../../app/views/devise/registrations/new.html.erb:22 -#: ../../app/views/layouts/_navigation.html.erb:63 #: ../../app/views/layouts/_signin_signout.html.erb:41 #: ../../app/views/shared/_access_controls.html.erb:5 #: ../../app/views/shared/_sign_in_form.html.erb:19 @@ -2412,7 +2411,7 @@ msgstr "" #: ../../app/views/guidances/admin_index.html.erb:5 #: ../../app/views/guidances/new_edit.html.erb:12 #: ../../app/views/guidances/new_edit.html.erb:16 -#: ../../app/views/layouts/_branding.html.erb:77 +#: ../../app/views/layouts/_branding.html.erb:82 #: ../../app/views/org_admin/annotations/_form.html.erb:3 #: ../../app/views/org_admin/questions/_show.html.erb:78 #: ../../app/views/paginable/themes/_index.html.erb:5 @@ -2517,40 +2516,40 @@ msgstr "" msgid "Previous" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:60 +#: ../../app/views/layouts/_branding.html.erb:65 msgid "Admin" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:66 +#: ../../app/views/layouts/_branding.html.erb:71 #: ../../app/views/org_admin/users/edit.html.erb:71 #: ../../app/views/paginable/users/_index.html.erb:26 #: ../../app/views/super_admin/users/edit.html.erb:105 msgid "Plans" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:72 +#: ../../app/views/layouts/_branding.html.erb:77 #: ../../app/views/org_admin/templates/index.html.erb:4 #: ../../app/views/paginable/orgs/_index.html.erb:8 msgid "Templates" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:82 +#: ../../app/views/layouts/_branding.html.erb:87 #: ../../app/views/super_admin/orgs/index.html.erb:2 #: ../../app/views/super_admin/orgs/index.html.erb:6 msgid "Organisations" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:87 +#: ../../app/views/layouts/_branding.html.erb:92 #: ../../app/views/orgs/admin_edit.html.erb:1 #: ../../app/views/orgs/admin_edit.html.erb:6 msgid "Organisation details" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:93 +#: ../../app/views/layouts/_branding.html.erb:98 msgid "Users" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:98 +#: ../../app/views/layouts/_branding.html.erb:103 #: ../../app/views/org_admin/questions/_show.html.erb:88 #: ../../app/views/org_admin/shared/_theme_selector.html.erb:16 #: ../../app/views/paginable/guidances/_index.html.erb:7 @@ -2559,15 +2558,15 @@ msgstr "" msgid "Themes" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:103 +#: ../../app/views/layouts/_branding.html.erb:108 msgid "Usage" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:108 +#: ../../app/views/layouts/_branding.html.erb:113 msgid "Api Clients" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:111 +#: ../../app/views/layouts/_branding.html.erb:116 #: ../../app/views/org_admin/plans/index.html.erb:11 #: ../../app/views/super_admin/notifications/index.html.erb:1 #: ../../app/views/super_admin/notifications/index.html.erb:4 @@ -2595,11 +2594,7 @@ msgid "Privacy statement" msgstr "" #: ../../app/views/layouts/_footer.html.erb:17 -msgid "Accessibility statement" -msgstr "" - -#: ../../app/views/layouts/_footer.html.erb:18 -msgid "Github" +msgid "GitHub" msgstr "" #: ../../app/views/layouts/_navigation.html.erb:12 diff --git a/config/locale/en_GB/LC_MESSAGES/app.mo b/config/locale/en_GB/LC_MESSAGES/app.mo index feea1ce128f4d4347e1f5367d33a9caf96b968bd..3293a5da77186825d5208872bc145500f38d107c 100644 GIT binary patch delta 38 ocmeyv_lIvoDhrpfuAzZ~p|O>T`Q&^SIVjh}%EV&xbe5+~0M)h%aR2}S delta 38 ncmeyv_lIvoDhrp9uAzZ~frXW+!Q^}vIVjf>$lW}h\n" "Language-Team: English\n" "Language: en_GB\n" @@ -36,11 +36,11 @@ msgstr "" msgid "Departments code and name must be unique" msgstr "" -#: ../../app/controllers/api/v0/plans_controller.rb:27 +#: ../../app/controllers/api/v0/plans_controller.rb:28 msgid "user must be in your organisation" msgstr "" -#: ../../app/controllers/api/v0/plans_controller.rb:56 +#: ../../app/controllers/api/v0/plans_controller.rb:57 msgid "Bad Parameters" msgstr "" @@ -204,8 +204,8 @@ msgstr "" msgid "Restricted access to View All the records" msgstr "" -#: ../../app/controllers/concerns/paginable.rb:182 -msgid "Sort by %s" +#: ../../app/controllers/concerns/paginable.rb:183 +msgid "Sort by %" msgstr "" #: ../../app/controllers/concerns/template_methods.rb:8 @@ -1831,9 +1831,9 @@ msgid "" msgstr "" #: ../../app/views/contact_us/contacts/new.html.erb:10 -#: ../../app/views/layouts/_branding.html.erb:37 -#: ../../app/views/layouts/_footer.html.erb:24 -#: ../../app/views/layouts/_footer.html.erb:30 +#: ../../app/views/layouts/_branding.html.erb:41 +#: ../../app/views/layouts/_footer.html.erb:23 +#: ../../app/views/layouts/_footer.html.erb:29 #: ../../app/views/org_admin/plans/index.html.erb:40 #: ../../app/views/paginable/plans/_organisationally_or_publicly_visible.html.erb:39 #: ../../app/views/paginable/plans/_publicly_visible.html.erb:24 @@ -2325,7 +2325,6 @@ msgid "Do you have a %{application_name} account?" msgstr "" #: ../../app/views/devise/registrations/new.html.erb:22 -#: ../../app/views/layouts/_navigation.html.erb:63 #: ../../app/views/layouts/_signin_signout.html.erb:41 #: ../../app/views/shared/_access_controls.html.erb:5 #: ../../app/views/shared/_sign_in_form.html.erb:19 @@ -2412,7 +2411,7 @@ msgstr "" #: ../../app/views/guidances/admin_index.html.erb:5 #: ../../app/views/guidances/new_edit.html.erb:12 #: ../../app/views/guidances/new_edit.html.erb:16 -#: ../../app/views/layouts/_branding.html.erb:77 +#: ../../app/views/layouts/_branding.html.erb:82 #: ../../app/views/org_admin/annotations/_form.html.erb:3 #: ../../app/views/org_admin/questions/_show.html.erb:78 #: ../../app/views/paginable/themes/_index.html.erb:5 @@ -2517,40 +2516,40 @@ msgstr "" msgid "Previous" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:60 +#: ../../app/views/layouts/_branding.html.erb:65 msgid "Admin" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:66 +#: ../../app/views/layouts/_branding.html.erb:71 #: ../../app/views/org_admin/users/edit.html.erb:71 #: ../../app/views/paginable/users/_index.html.erb:26 #: ../../app/views/super_admin/users/edit.html.erb:105 msgid "Plans" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:72 +#: ../../app/views/layouts/_branding.html.erb:77 #: ../../app/views/org_admin/templates/index.html.erb:4 #: ../../app/views/paginable/orgs/_index.html.erb:8 msgid "Templates" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:82 +#: ../../app/views/layouts/_branding.html.erb:87 #: ../../app/views/super_admin/orgs/index.html.erb:2 #: ../../app/views/super_admin/orgs/index.html.erb:6 msgid "Organisations" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:87 +#: ../../app/views/layouts/_branding.html.erb:92 #: ../../app/views/orgs/admin_edit.html.erb:1 #: ../../app/views/orgs/admin_edit.html.erb:6 msgid "Organisation details" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:93 +#: ../../app/views/layouts/_branding.html.erb:98 msgid "Users" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:98 +#: ../../app/views/layouts/_branding.html.erb:103 #: ../../app/views/org_admin/questions/_show.html.erb:88 #: ../../app/views/org_admin/shared/_theme_selector.html.erb:16 #: ../../app/views/paginable/guidances/_index.html.erb:7 @@ -2559,15 +2558,15 @@ msgstr "" msgid "Themes" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:103 +#: ../../app/views/layouts/_branding.html.erb:108 msgid "Usage" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:108 +#: ../../app/views/layouts/_branding.html.erb:113 msgid "Api Clients" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:111 +#: ../../app/views/layouts/_branding.html.erb:116 #: ../../app/views/org_admin/plans/index.html.erb:11 #: ../../app/views/super_admin/notifications/index.html.erb:1 #: ../../app/views/super_admin/notifications/index.html.erb:4 @@ -2595,11 +2594,7 @@ msgid "Privacy statement" msgstr "" #: ../../app/views/layouts/_footer.html.erb:17 -msgid "Accessibility statement" -msgstr "" - -#: ../../app/views/layouts/_footer.html.erb:18 -msgid "Github" +msgid "GitHub" msgstr "" #: ../../app/views/layouts/_navigation.html.erb:12 diff --git a/config/locale/en_US/LC_MESSAGES/app.mo b/config/locale/en_US/LC_MESSAGES/app.mo index 345286a01220051b1a803c9d7d5520e8efeeea6a..a00adf9b800b12b7b4cd9abcdafc6cf0f2bef3e5 100644 GIT binary patch delta 40 rcmaF6nDOmm#tmC_xr}uU4HOKGtxU`(@7I-sa!ssEEH*#aWm5qF7_$s5 delta 40 pcmaF6nDOmm#tmC_xr}rT4HOJ4tV|6i@7I-saxH<}&Chk&Q~(%a3?={o diff --git a/config/locale/en_US/app.po b/config/locale/en_US/app.po index 97e1c815d6..e4fb4c00f1 100644 --- a/config/locale/en_US/app.po +++ b/config/locale/en_US/app.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: app 1.0\n" "Report-Msgid-Bugs-To: contact@translation.io\n" -"POT-Creation-Date: 2022-02-10 08:50+0000\n" -"PO-Revision-Date: 2022-02-10 09:50+0100\n" +"POT-Creation-Date: 2022-03-10 13:47+0000\n" +"PO-Revision-Date: 2022-03-10 14:48+0100\n" "Last-Translator: FULL NAME \n" "Language-Team: English\n" "Language: en_US\n" @@ -36,11 +36,11 @@ msgstr "" msgid "Departments code and name must be unique" msgstr "" -#: ../../app/controllers/api/v0/plans_controller.rb:27 +#: ../../app/controllers/api/v0/plans_controller.rb:28 msgid "user must be in your organisation" msgstr "user must be in your organization" -#: ../../app/controllers/api/v0/plans_controller.rb:56 +#: ../../app/controllers/api/v0/plans_controller.rb:57 msgid "Bad Parameters" msgstr "" @@ -204,8 +204,8 @@ msgstr "" msgid "Restricted access to View All the records" msgstr "" -#: ../../app/controllers/concerns/paginable.rb:182 -msgid "Sort by %s" +#: ../../app/controllers/concerns/paginable.rb:183 +msgid "Sort by %" msgstr "" #: ../../app/controllers/concerns/template_methods.rb:8 @@ -1833,9 +1833,9 @@ msgid "" msgstr "" #: ../../app/views/contact_us/contacts/new.html.erb:10 -#: ../../app/views/layouts/_branding.html.erb:37 -#: ../../app/views/layouts/_footer.html.erb:24 -#: ../../app/views/layouts/_footer.html.erb:30 +#: ../../app/views/layouts/_branding.html.erb:41 +#: ../../app/views/layouts/_footer.html.erb:23 +#: ../../app/views/layouts/_footer.html.erb:29 #: ../../app/views/org_admin/plans/index.html.erb:40 #: ../../app/views/paginable/plans/_organisationally_or_publicly_visible.html.erb:39 #: ../../app/views/paginable/plans/_publicly_visible.html.erb:24 @@ -2331,7 +2331,6 @@ msgid "Do you have a %{application_name} account?" msgstr "" #: ../../app/views/devise/registrations/new.html.erb:22 -#: ../../app/views/layouts/_navigation.html.erb:63 #: ../../app/views/layouts/_signin_signout.html.erb:41 #: ../../app/views/shared/_access_controls.html.erb:5 #: ../../app/views/shared/_sign_in_form.html.erb:19 @@ -2421,7 +2420,7 @@ msgstr "" #: ../../app/views/guidances/admin_index.html.erb:5 #: ../../app/views/guidances/new_edit.html.erb:12 #: ../../app/views/guidances/new_edit.html.erb:16 -#: ../../app/views/layouts/_branding.html.erb:77 +#: ../../app/views/layouts/_branding.html.erb:82 #: ../../app/views/org_admin/annotations/_form.html.erb:3 #: ../../app/views/org_admin/questions/_show.html.erb:78 #: ../../app/views/paginable/themes/_index.html.erb:5 @@ -2533,40 +2532,40 @@ msgstr "" msgid "Previous" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:60 +#: ../../app/views/layouts/_branding.html.erb:65 msgid "Admin" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:66 +#: ../../app/views/layouts/_branding.html.erb:71 #: ../../app/views/org_admin/users/edit.html.erb:71 #: ../../app/views/paginable/users/_index.html.erb:26 #: ../../app/views/super_admin/users/edit.html.erb:105 msgid "Plans" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:72 +#: ../../app/views/layouts/_branding.html.erb:77 #: ../../app/views/org_admin/templates/index.html.erb:4 #: ../../app/views/paginable/orgs/_index.html.erb:8 msgid "Templates" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:82 +#: ../../app/views/layouts/_branding.html.erb:87 #: ../../app/views/super_admin/orgs/index.html.erb:2 #: ../../app/views/super_admin/orgs/index.html.erb:6 msgid "Organisations" msgstr "Organizations" -#: ../../app/views/layouts/_branding.html.erb:87 +#: ../../app/views/layouts/_branding.html.erb:92 #: ../../app/views/orgs/admin_edit.html.erb:1 #: ../../app/views/orgs/admin_edit.html.erb:6 msgid "Organisation details" msgstr "Organization details" -#: ../../app/views/layouts/_branding.html.erb:93 +#: ../../app/views/layouts/_branding.html.erb:98 msgid "Users" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:98 +#: ../../app/views/layouts/_branding.html.erb:103 #: ../../app/views/org_admin/questions/_show.html.erb:88 #: ../../app/views/org_admin/shared/_theme_selector.html.erb:16 #: ../../app/views/paginable/guidances/_index.html.erb:7 @@ -2575,15 +2574,15 @@ msgstr "" msgid "Themes" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:103 +#: ../../app/views/layouts/_branding.html.erb:108 msgid "Usage" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:108 +#: ../../app/views/layouts/_branding.html.erb:113 msgid "Api Clients" msgstr "" -#: ../../app/views/layouts/_branding.html.erb:111 +#: ../../app/views/layouts/_branding.html.erb:116 #: ../../app/views/org_admin/plans/index.html.erb:11 #: ../../app/views/super_admin/notifications/index.html.erb:1 #: ../../app/views/super_admin/notifications/index.html.erb:4 @@ -2611,11 +2610,7 @@ msgid "Privacy statement" msgstr "" #: ../../app/views/layouts/_footer.html.erb:17 -msgid "Accessibility statement" -msgstr "" - -#: ../../app/views/layouts/_footer.html.erb:18 -msgid "Github" +msgid "GitHub" msgstr "" #: ../../app/views/layouts/_navigation.html.erb:12 diff --git a/config/locale/es/LC_MESSAGES/app.mo b/config/locale/es/LC_MESSAGES/app.mo index 940999e8f22eb356ca27529f803f9ea39a1d6a35..4664c9572090ea6547cc797d096c09d059bc7f93 100644 GIT binary patch delta 24352 zcmZA81#}h1!-wI$fk5yO++&d7NeJ#9g1dWgcjqFdxDyR?w)jc$y_ znV6X8J9mhrBjXwB0X}UVr!1C6^>h>#!`YYuFXC~0h~pXY)^?6FjdbY_jNp;(o430#5`P*a$A-ujO*n%*SedfyWLYGcbw&p1FqF zQyE4x|GJ^bXuDaE4mw?JdZ2X_rX_!hbrpJ(K7`sk$1o}0MNREXjE7$^K6;Na=_IK3 zQ=;noyNPI1WSR0GrMAVcX!$f!nHPzQK3Eo2= zd}+%+SmRB0oCM_iA^$lU`6H3e|5_qya0jw6odc)`7MNnDtUIcMYf&A%i(N3)RL9AU zBT>6~6KZqipXNACu@BC~^Qew>oNi86fAk|g1(Wc6XDN|nxCxWuL1g1PXHZL1Zw9Yt z9FFDjA;!hLv1a6vsN+=>eQ__|!&A0=Grj0fdMB#Ah*_osP0_8MbSIJ$M`8(_jdk%7 z#$fK*j#CY%VST)Vs$Y1H*<{r)kaR=LgM%?Iu0gH&HRL2a@#mT)7>AmX8FQI`t<@4T z61unnwG`XtnKj&JJ&Nl28SIN6QT2MxHw_I$ZMrd-5MyorBGhhQZ}ZQi>fJ=m;N$ts zzc$4SGPLVIphlK%fjJ&|uq)~ESPOr_f z7;0}^!_@c$Gom}rA~SM-)Qu%k9jb%@*bsHYAoPp?HL}H+4L6`3@F!|&AEG+?7FGWX zs(zBiX0v8M)eA&s+U?{eqNyl{nXn6L2F7Cm&O+U=6V>ot4 z^Pt-4f&MrIkK;njfgP8sV{SGJk&OgPIg0nClZbxt2hiuT@%6wMvy$>(&M=u<_ zlFs4_)RJvl=$ViAqF)LO;O<8Bu zQuMI-V=x=(^{9?sM9sh})J%D=H6K!b7(lulYNTyZ?RUm=*w3b;*RlR($f&!{Ja9JV zA-w=~Tuz|Y{03?yud$Vj_k>M1-C!OZgW5YiP$L|O$#5*HojIucS7Q>~gkHGcO+@GW z5PHrhYOSAOVvN7hm=ZNp0jPQfPz_Z?J+LmS12L%fy4d_5QF|d4d9^sJQ1?Y`GBe|@ zM?^hqf@-KWYRY?}-e_Y{4K7ACvHTCMW$bYRTenHXTWftx2asP5l7W@%;f+ zZvxU`w=jy9SQNkvjiRiKwSNb~w%?ZoG)qNyo6IJL4}{0t0sQVM2KeRL4f` zG2i{Bq6V?K7yL+D>nT%YNkF~lk8XDp9e`&y`}ZH!vV7N~Z*S%+B1Tj!uV0TosdQG=UM9oT0poW^9NZ=lxjDXRW^ zRDG}g=7Gsj$1EkPJUtf1U<|@$SRO~9>K(&EcxpfMuc?fGz&s!YYBOfW%$OB5@^Yw- zG{h{}0W;uOREL+NHtjalCOe7w@gknaLRDgZ)D1?h z;ds;({)Az;4)uVWsMGPlmVZIrpZHhv9nudMk{*NVaL8dZV-->D*1?R}-c3Xg7=>|g zI>yCWs0QbwI<&;P2{qyas3kaSy@{Hk$F}^VHSrNsFB9rHIZ#Vf2;-u=3=uCP6;VBn zMm?}2=EeaSh%2xJ{((6#!Ea_tBQccpRIG-(&`;$@%}6t%9vFt|coEcQuZ*`k{ZAB`l0- zb^c2e(E}P`acqy?_%kNNwbt#}hx8%TQWZLGX08~jBPCHItASeMwipiwVp<%ITB5nA z_E)1X&v!NwQG*9jBR+y^;2i3O@c?xS-lL{A$qAG1kC{kEpvr5YPC*-+KiKBa#W3H!}xG5VY_KZ2z~?TMPGj@3h7Y>)AA0BVyB z$6Pobv*8X@hi;+TeTHiH6YBku;4Jg+8QEDgg*j1ES_-x6o1rQWM{Uvts1fZz)w_XN z@FVJh8P1ve%3w3nZ7?4m#uoS<|G|dm`6R?L?hED%$WhehOL);dFbeg8i9v0i!Khbi zEEd7j=&Apw`LK$@tjpw>JmY6%Kj%OmyOPHiH76vWtqei%b~ z6gI*~sAE+As%fYS?jqd)nRO?{H8WH9Q6v0}MbQ7cnYqTOf%L{?I2g58CSXdP|CvOz zd)HtN+=kil4r+=L-7q6eiE1F!rVF8Ft_r5X*0y{QY9_{`Mm!%g;%d}`kD_MytkOK+ zxkp4dJVi~_7ZqT_o94!psPb^sZZ3nGk(#Lb4Nwinpz8I-5;)qXe?xWfl1)EHweQ?w z{?%YYA{tpb)C^=t%|IR0aT$s_CF4=YaH>tuMBP6h)qypr`g<`N|3KAGb=%yZ9<^8e zQO7j$HuFE4NJ}!*qlZ|`i?7+JH(J;|b3=L5h#R7&v;}HSyP`VY7c=9Js3lvD>ezZb ziF21YNWXznoZsaH6vY6d#O}sbv)mop65>Z*mS@bwRXX%-5QQ*u^Or)?a(vj zHr*HVl0O7B@^#o5FWPjOC+0(^BkGjQL)G7mK6n=s>HI$?qK4nug0HBx^m%G_a|mh% z3ZXVx1+0z@P`iC4mcyNx5WipoOz@0v(wG7hV-wWWcd&NF1Umn{i0H;4SPaKtc07Wi z_#E|ON&B4Na$r%^6#Bj3ZHb|{i9PWb<|IAyl^OXVEJXS>7R6kz&1Xk@RJ#-L49|DA z5gCf3-|#xcc>kJr{Z!PA$1w{Ji2v4%DCC{_BGMfrD1U(JaJu)7(;Um~hps-bp&qd0DtL)>Y>AMMD9iR*Is zBz8`qImpYs)5_6&Gi7a%U`25S;F|HUNQ_IodWY0oCI`AD8EEI+|k`=@l4>mr>_DNdlK=GlrlVj7BZR093>4up!>UAS}$URW;R3 zF$YdZ)!&C&TK6lPku{OaDMdy@)LPEPU_6R?@Dt30@e;c{yE+)vU{lob9fNvhPr>xK z982HHerAC!zq{ymts=ff+_G2YIFXH zY4JAdJ@Oyw{AW}^NDDrkjTA{Ywg$^B49=>2m5}y;NqmuftNLFQGb?HnlMbHK07`)^09Aq$HNX z%{T(vV*WH{Q_e>V=YzUj)Tq7t{mK zVHNbvYK~n~)If$~7ul$7KrY zc&$Y3^6jV*+(OMzygcT`k{VS%9MxbY)GN3as^R9S`t4BXy&vj*G7+@|Gi-Ve#@G2@ zL?j~xD^V|$Bd9mvdDI)u$!pduCnh3U0M%d_)B`KobWPL%8la9_I}E~}s2QG#d2k2j z#lNwi&VQOPbK_*x0~TU(T#Xvp9@Goxchq~~25KaqP*dy*Hyw(PMM)>c)EJE#SWDC? z8j9-RUQ~xppgR+hXEq~ogv;~y_j#~1`E60J+?D8sJ5eLvgX-8}RD)+xZ_3-K5x+*w zK$?7}qk*V;A*c=)Kn=85KF+@yj3PtUKz(>LKuvKkn?DePNsmB1Xa#DGcVQs@ff~_M z)GIkzq}j~rQ0?SLwOaa!co*uy2eASEj(U#-7BKfWa1&9_o1&&v=?I*Fom~6{0dB*bg&nRwANx>3k#;T)EO)J!18i;MsJ&A}m%^TFF2`g#N zZ$;GJsEiq~16IIMsF^x}`pme2>UgG7W+rl=$|F&0TpqQVYogk3jkRzfa@yR^Ng~?S z*RT|Rz?xXJw7Fp-Mv-2In(C+c4&UK!M*O^t%k%sHHRW8MzpN@*-sPO4JW&Pn;<<`f zNWVtyos$(!ySFig&i^|i8eyU+^9BpSRiyi%dYq+_%Q=7rumHZZ=81MW^GLVBIq0fv zK11eVdD1tL&o3vqipv?o@hM%^ypZ-+H=Fk&X43h;Pef}Nr-tcBTGZ4>VhyZ?ad0VW z?N*?sem7RckEj_dThnwf7PW^?px*UgaU>S0#rFo*WFO8ZJ+&F<{{)d7&0WrGe1~Q6 zaSJ*^&-1o4AGeiTnKc`Ol_|f5SumuvS+a_lhx8chcDzXX0p`LzF)n8JC}>>i>%BVMA8 z;|J6paJ4ftnX;YRtZ^tAYM>-)x7I*?YPCW=co06snOF#iwKomz#Nnh*U^G_iVDjgn zIfK)t)sdE{cYiljhlk>F9B;PrpHU+((ZhT= z^+Qeh2AdA=>GJ$Dn+{lse5aSoX$jG|5Z9t+wsLPXlZ{cIiha>n=YKj8ZHi^6FAUpp zKAuFK_l|vBPAo3QMp&?~*-X<=GcgCJ;1bjlJ{4%>)|5ojqk7-b{^>R{PUaRn3;6YATzQu7)rVpmcc<-4EJFX{D|wY zz+m%!`3uzn{ku-j@BdN~DNB!o(DTh@s9D>dsI?u6`qUbOV{sp9WKoHNwxR8H_j5m>jhyGNRfGM;+%97>EtsMD)#Ou&uBd_2OBNA$SyZ!*kSw z6aHYPFeT<99fayg4b;rFMvZhJYBT?cnz8w)0jx(2a4V|5`&S}`iCjf(qLiadPm80L zBpUUA9;gxifa=IZ%#E{9FOnm+{9lYB{RK4>6@N6}qHE!=q=%rMQ+l*#Aa184ky2y~ zKsCGz)$_wR1h1ktO@lG^4TpN;4Mv@wiKr1K8EZP41=Z0AjKrd-k#|RpydM_CQJ7vW z>?NWpzwF82!vZyhiN~26(xN}FiZ;JG>W$ab+6U8-o@mo6P$S)sLHG}VYdzOSm4j84p_jM7O^AJSL*4k3YqHIweQlSO&Gml~9|lA7;m8s7-qcb=+Q~mL&dE za~jg3_C!x>VJv>aWvKQSPviX8A+mCs$@q*~o4C`>j1)sXpb4s>?#MoM`lFWU z^9-|e31ZER1)`QDFKR#)QJb(P>VZvAGte5nuzxJ)U$4?ZWa!4ZsPn!VHKNm~V{+T( zzeCMf%9&=-e`4D?KMJe;sK}^&Scz-n^1c#W|sN>Kg4Y#BT#ET8g=6g)Pq-B z52DuaGHSO!w&~BPj{43v9SA^u+J#^TEQs3mi&3Xy18OFApxSZoC!(o3i<**as0MGM z8hVZ+@GG{!5p(SCZtx@NQgiKZO6Hj_rTdVdJ)AFCi1K0!%$L+Y*pBpGY>inLdVT@n zcE%EELWbW@{1%HFyP-ZN>nwITJ#j8pz$8of^rO5EYNpmMHA}Sw>_|D8M$J~U99V+Cqz_MmqCDO7_WaTTUmZkA%N^$coC@1Zv7 zBh>w`P%|0#XJaaBHfurj(fN-eqP4Dxy0N`=pv|9v8sSpZQf;#NyKVX$YV%!3J@77; zz}Ki3P{a!J%c;hwc2;63+=gyV?CY zQ#=gysvUz`^CcJuH{o#Hiifb^TJxg%fa+M5b>=wcz^tUJtmFJ^lk_A*Q#b;(mSb#s zGHOX;tt(J3q@Ab-pG4h%-IhPGiMNH{ccAk7GCUV>(vaVrInq7t>B! zo1sosb|cog-($Ef=fZ8slk zSy6Aqrl=0j#3-KcY$2i<_=FdD!zJ2bD%{>__P}S<>4?~6rm!IDxJIKoR1ejG&NhD} zYJ`(frzX~xA4P4tn;3?Fqg$II?QZjcT&Q$7YO2bkUcHTM`C`;2{TcP%ID&emoWCDhdZhyIv(uQ>%JF@kg} zEP}CE6wjchKJh;DtvL*PlWu@I6(_MdX5H`d{M(UHs7+bw0O!9nkxvKA8>_@Y^T1ft z8f`-zzw=lH-(poPd&vB3I2CJ>K89NB^uL;q-AF7?dLe3N{=)qD9`!!SbJ#3JDK`mh1$J8VP0H;1@RJUO_LpQdHz1XH0n*c0JUUWQA_d%YDVv)rv4+UBZ+@A z_57{w{6thS+Ge!CQKb7|TiuA-G_8)B)6oepkRE_8HfyzGW>3`k-F!B5!^)J;L9Ow9 z)cxLnn2sbxy+5)b9dT;C z6l%BE#9A1G>d1D~rao!Y&r$bzpJX69|H+AH&2wQztbn<366(0@LG6X(sB?YZdIj~s zTc{DfLT#>Or_Aqm15o#kMs;{HdPa^qMXS+$hR6*fT7vbbUCv=Xjdr7EWY{_LVKECe zbsJIjcA=K$C~67LU$SEDc1n?d%{tunOKV2 zL#wZF{^)E;oInkh?!+DutcGZ2ZIfzqfqVl=9Q9Z>BoL{0rl?1vk% zCWc-!OFa;E{|q+~e*f`m29?f;dSDQ0h8kci9D#a|+{c2L z@P?V8GMI~W4C=vCQG3BX*Ji9oHM|{j;y%<;JVNaSubU>F4)v>;aMTxx$yfzfqDK5L zYUVy;OZ2&A2G$YP{vgyQ9)Zl1+ZjVdH%!ACxB!dbGt7qBZ<}{}4SYyJ|?zo&+ z*blR?X*=FE9Uu3X`31x_)F%6i+7tQjn;Gbgnz1>ke}CfqN@NNdhaVX0{%wAWeSl@i z&-oAEUa8v^mA>}S{2fu|N9F;uQ8RW5HR2EW2%9`MA2Jo6FaYk4^VH?}7n32+%m9X< zx6c0zB7ry;Lvb%=zz3*zzt?j!f(R^2x&vx0*I`4vidix8g&BDx96)+0p60=sUYhsH z(^uxhEB|YA{zszc|Nq@aM91(XYE8f5I!ybJ%m;6qgX691Zy+3u*XZue0g zjC^a3Uw_ml`~}1C2`0x(@62Aw`;Pe!q=yB`=ubhT_b$&rk3El*NbmX27B78>ABXcZX(6WNY76J zn!*OCH`fZ(XTUwwyFIzf%ky5ygKD4}dOjmiA0~THFQQ|Z1^-4ZReUcm&u2>$Y(Tm% z>J@w%b)Wk_5xo#zqSp8WYHI!BmK)h1vzr4^QyztSQ?*2`bsy9SCLk{mX9))2TGWV6p&oPczOQ$ z%?FIjCf=UV%kxw3o7E6%!O6KMHx?LK3kRF736+cD2(2DqadH#{ganzeIEV-BGz0nqH zk)Dq3nM58C`3Z;kd3j!ynNoN;d3dGPK|N?>DlgCZJ%lAmzea7oe5t)WZ@!kOj!r~v z=F_MbQ<5}Z&RAxq9qLrQOzY*8!h-3%Jij&XpU%teIp=%Hh@zlMdSfgOBz+UBW5Wz) zhE}0EbRSz`a7MGMM`IV#w=n>#WikWlj_paWK*reTkbwiM91nP>bN~Yt(A8+vzC=H9qF+ch)YrD_czq8zKi;j`U3TUuc-Gz!Vpt0 z6RJanQSX(~){4mY0k>0?h#IbgzSs}7TgRYYq5Dt`zeJtyPpB^vKA~oW$x!KZsFC`k zI+h>RPFd8R7=V#D57q7&9HfICC%bu}xKTZiMV-%;sFCeJ&CpQ{#hVxx z(Y2XGPQv&&COybnTCH#eB0nfQkH{}VJcO`;^kK@Lk=LEjjC5jxt_XX-&vaMC6z=lm zwj!rG4yAMm@dCt)(@HevwzWOC+CTE~KATG5d%klATM9#YaAv{&#I8HktBTgdoCZ z(iJIp*?V`|nq$a2N@!~HCz1af;Yaek2)eFYb5O^vGuW1pf`>Mu;WBna{}I>qE0x<2 zKVlm^YxT4BB1!9IuIrs`nD6|aYZqyq;Rf8lgS=6;JR#{uq|2i|xjftRB8BtG7>4{h z*11joXToaobZsYZFCm0b&o-odow>)i$@e7lN~uj>fv zOS!IewtNQh9pumSq4bD} zoV~X#@nI^r>A$U&$=8>z&ZK|zJcYAMoUcHhD<0+U=_DEv(RGstjOT`*2@#}yur}e1 zy}_6G65=ljzft!en_t7;&sQR+7In_yYQh@YI7M1lWx_4uuRJOK%{=9U^mRHp4e@Q$ zX=g{L@}pF|Vbg!u1}5Q26;fAMCLa2$ElWfm-zYto&Jf=`oC$c;WI2Ovy&gE))@wog ziS@1fip@AgMiJsOs8odb0i0)Vd~F*lYe%YNHR}F_39&NunqoJ?09!YJHYSm-gG(rn z;odd2JyqFByp)?i^fhn-6;D%9*D2x$3AzRmQc!0up(+hzBXlO-)7Gtzt;rkgsm-i$ z-zR&ozQ%@9en|Cgc_DkBO*-x;HbY;sf1skSEO^R9oqRO@pN%i2?mikXj57&I2*)U^ zM*TqIY3==ss56Ur6Y|#D&eX*+ww+Iu{Y*OQ+b2#|D(X6H8<OCmo*N4hHW5-%r#)^b ze>L~$`_c^3`g!S3{FA)usOyz=FztN1_-1Ivf0xVxRM_=h;Sg$VqJe2xlaP*wrM7pr zq+Ti;-(>G;#{DU5d;?|MNk1Sx%RYEMZRH{8iXgAHXMjAv35jB4hEqAO9d)?9DL(1v z+|+{d5JD$H6nS;X`)upfBi+M3cqQef$^VV;6Lmr<>qOaELK$0!DxUvoN9L_U<|W&Z z7xAlPzP7#WK)j?aYk?`r_a~2EV>;n9{F5z9MmmgpYfi6PGlTmvH3m?Ul-y0k{RqDH z(E;3bo%nsqe!?jPea){%{$}opi_^#(N4dU5?X(@6gDLan&u_RWdJU{nk zC2+Z&?);gU%2hBIHl@M=jHW_%;sXh(N%O_pbMdPM=P7x=ld5F09Dc#-xgsfBLD?A6 z>&S^T4{?T*x6RhAYTHqAq`pG(1GD~54bQ)Jtwj79m7f#%s_*2$Z&x|$G$ii{VK$iy zaME|>TAM^ReUZBSO@LE@4lE(>IrT>nu5!;J(qC=8YKe(-CS<4b51wyW(}<-fj3A`6 z&z@>+Ox`Er&1u%3re1Mp8r1bW9;ZARd2dLs#aKKDu^< zDR(=56doiq4Pig=c7!uDvXKfei0cX^zMk@Cc7m1nki4{{m*WM}$0+YlD6B%RK5CC} znNWv%w^7$!>R%>oC1lYjY-$QeQh1BP!rwihtbO?3_QvDfklf}iuzFhLkL|Y3L#y(; z+By?$rE`>}r~Q89x1=pyCBsPU{(p@n`mPiI*am&6>`h2CeN&wj?ymNBH8F(v73`)2 z*DBIkF%GTXvUg`D{*b)5_y^^4$Ula&@e6r%3A*mv`ij3Ko{4<@96Fe~eTioxZ#ln= zcj6LxPsVr(9^eN&tGl>%Qr4c3kbGV9DXT)f2=O1tPlAQX)0K;K9;`;VM3_f@ONKwe z-bE%cZg4?(quv#d(#KybL}Af zOnC>&zFoC=NH+3xU9u`(pYRK5f6DXI&NbVX7x!i)e0#pZ8dC3UH$*aUYR|r0&LkPMO(3yDD?V|OcXYxG1?D$B< zDpcxkE7~HbBKboJ2?&d9W^r35hKAY@?hsy3uB#>ws6hFA;uVSaBEFD#YU+Hu(s}AK z{yny#8x+1(L$;Cf)SUaRHl`$hJ>_+A2+591ttE&iSGH-*2TuKNb36BXLpM%Hm-MVp_K2S29nWm{j_X^2m@bxTvX3n9?f zZ)N=l?-4rMddhaYNtC0(iQKT*HdxeJjyzo>2`9)~W7Czn=Pv1GWm zeF(b#qTXEc!f5~7wVCud+YWhd{@R8mAQnjFSmHZu;VO(FBp~0H(1%co@}uPOyE3N` z@n3D-0PZhEUU%}=5`RL_mCQakhO!cr&BSllJMQhmFXKGF{O(I)gDo8UU8R)Y#XH(N zvtwC80A<5z)FA{DhLg@oI~mBThY!gsLx>>MC2s)cvF#0`JebhX%^zI}XDN72_)MjH zq#JR=_g7!a4wHY+HgJUaVjEYz3#4_`BJV40=xW30x0Wwr&o}+yS=o zCo-4X21k>(l)}a2ZNW4&wC=lyY14kY8W8`Hu*~MAvKFAia6)DKFx5XnI7Rxc&D-_u z{d1niw-9pkpv)Lcg&f4Abr)A8=@m3gmgj2EpC`#%VAD+~yJ#EjPhNAv7us1v$jnU{ z@ej%i5$}Z!sXq#%wf_4lyhnzv>14)|9)tT#mh*wK#^n8H>zttM+cla+tsFzG(Tw`eMpQZR)?c5cebgDPP=+t?4Jzg??rV+*OjmUvNH zXCd*?wo{p@T#)!z?(?5S^b1_@#Px7AGI*YBr z*uXYk+vYW+{udi>iZ=*_2-j$D9QMNF)M-T96^QGqt{;ofkeQ8)Js3%5Dk^j*JSL1J ze18?>q4f!m$j^xRxkuN(hX4J!mHZBb>Et{mG^hP#+!IQ?1^2}x9*6Q`aasQnL`qRm z0ska4Ca&u?H^o;OH}p1H&I@jwW#jt3^^Uk7b)FJWg{MjXOZ*(+`|B$Sik+3@Po#|m z#A68hG27X>C4aMP*|&s_*%*@4so0@+RG(JT! z!}Ar1ESNKlD`Ac5xofrQ+o4y7Ze4SiYuUR^5&wv=h=|-_re0)`$U@r(7j(_cyFIL( tt5=?MG~2#UE2l=!Zml}E>C&>7f9q~t{q?NvdtzOgD{Q}h&Go6y{{i)pU-{3^AIaAXDdF$ zwK%(rX}4lm$5~9e1t!HWsF6q_y_6*26cCb*4d7)NPotdK92LEhnc!Jz4Yde^9{Wkk_1s^$O2FgER5<<4J?9T z7z1aahISrm%GRPr<}NaqPR8DjQyjxl9bJI=@dysXw-_frQ__d&B|Ug3<9~#RO5eg! ztbsd*Gacxmmfpx2X&z7#%aa_2TC7J<9k_$pF~caw$%&P49Y&z?(~LIha;P=)0=1^{ zjA8tBL-jFsu^=6EqHTJ-bta}Ef3bBp`jS46T02)U5xzqWt!Jzm$wU~7basr5`E0r< z>N%yoM08^aYH>71HPG4G*XEC~&cqXxufm+zeVpSY#5u^ib5>#zOfuf&2V*?a^-=A& z!cy1+v!ZuB5v_r9m>Zv>-Yoe9Gel`IIq4FpHBl1_Vq1)lKcG6W81*6>ZF)cIMNXkQ zb`|NfQ*k20u6!il>wL5sVJu-Ss!3P~k75PV>9nZwEY|!Omvj(L(f+S&3ofD>yo)SQ=PBxet)`kGn}h1$MN|jlO>>+`EQ9%Q z9cooyLoKqF(+Ri$XW_q?5U0;DYhy9G`~N2*DmaCS@fs$=XUGC}KB1;2`Ul79h-)wy zlg>0JWnfEnxm*5=p+m z^e7GLV90_Ri3*qs8(}w$#CmuWlVadPvpXtaI?@d>HTFQ2k41HCo=vYtP3<1kNS*W& z(LR2On){TC949koL#^6iR8Q-o4xm=3k?DteL$56#k3pnoqek*PYKU*3>ODeD@f+0I zh_Tqbm^THH3}ocNOc;W?u_LNOJ<%VBp+;gEx^IAbvy+$|ub>|A71iOSOH4;Iq3UNx z)h~)#td)>@UZ*+{4RvGGNJL;poQWENZRn2&Q4QZijle6^n|{Dt_zeRv*HV*T3oDXt zglcCl`r!&Zg+F6%?f>b^OwaaXISNjp8jio*ym1QDw#kF)z;aB9TTpLy0tet_)SQQ| zF!^oKmvm3eg8eWi&O^1c2y^m$XFU-O-6c$pPf>IB*&2JLxgi;9k!3~=VG!!gDxmJK zWo?5R(I`}VV^JNLkD2ix#=<-3^(FF{i00;{^&ixlh_lLcBpYhT3LyV;KJ!l$E?@09 zhcL|=GiBFs59yCs2Dh#?YvT>ZBAt4j8KF$5kt?u{@mHiA8R~Iu)SI`%Y}gGoWIv!5 z-&~u&8MBkVjOyqY)CiH~79<^n{uqIJ(aEUxf57v&Xg%Yvj9weeVi|&Z;33S9 z$5GqmFVvj-ZZvO_9@}~tR+}ER$vk)>YVFL$M7R_a;}%pqhcO|ZM~%o`FA*OiPf+{& zIlB9EvzhA@n1K91YiZO-Rl_9M3e{kD)B}g0IxrE{-b|an5w#ZfBS(#M4t1Zm#}+d* z(WoAcL^U)4HRSV9C)yTNgC|i9-A4`i2b+$&)y!>1)CdNmI#LANVHwm&FTvQj9;xSb zwi8js-KaM`f*O%Crh;=GxSX9S0>@(j1 zccWh5g7q#YC;bO%s6G2lIx!|C9U!&;ixNqORZ)wq879FVs2hi2UYvrO`+YbW&!HaJ z=72F0HIf5SQ!oNGm19xu%(kwuZbPpM4ikxsXHX4ZLv`RWs{A7+M&E;G4pXA)`=RRR zMm?}NYMYg|6#dTqcIrQqUyal$oQ8a@_`HuW#A$6fKsT%SOqg-E!3MwpgJ-P zGvhQ&k6Tb3{tdNgZ=)93dn}4y@C+6{%(oH)Ein)?!=%Cn>HFM@qA2p8gJREO&vGb7d=)$U*~kqktpq8iwMF>o)& z#Dk~?kD)sBtMwY{jh~{X;BRZJ<7R}CqsjxUMNsvEQO~J|nnG_IA~A_{K_Bdn>S-_3 z1E*s?T!LBf43@=rm>UcJY=*Q2<|MrvYvX;?qAqyCylG|B1Dl{a))rapUMGr(8XAY` za69_r8C1vKqI&)T)$K92Q#4>%!g^Qtj%wNvq^WvRrn28#osX=UbNmp&Fyp4RJA!}My@@oBOQ@9bNZv^crwPqrKsnuK`r7V z=*#n+^F(yxRaAq|P;dMPs)2t{2S%dPW*7LOhPEgwKLj&kGh5ytwF@TM{N*Gfa0^fmNXg#GhB;6pQVUhDzDj82@}kZjqr8h=0~RAOq?FSug<>z*1NSwI&9jIu?yda4N>eC8$NV z2J_-J%#L?a9g2O*zYdUsWVmmJ8j<>_A?<`(^`lYs)}R*Yanu{#Mb+~? zZ+1-p>VcI|_jSQmI0*~mOKgLF7udQu%u8e>kuDd_7m(Me#aHN(d0-FJ!88%Ic$TA% z)cqKQAJMIU*?d^_z@+3K!Y+6ot6`ZdW~9cTIyMJ2V&1((G)G4<6`sZbe1K{w_El3c zE#@cfZwx1ttnoNMMm889X3e3%$3q2|0kYHC{>y-p;NBxDT2WH`}QScL6K zZ@^}l?7G=Tk*J1x;~pG}%z{(uh8d}ZH_aPn!yxiQFb$4Cz43fhhn8bp?f>mWH0K9U ztM>xt#@m<+p>8Z~%bTKBa~IUXG5}S7C~C?kq8_{u%i<=Reue7bH=9mHr#E~2T=DPLv`Q+s{SLaj_*+Q%iJ~hS453u z$X&)?`?LiaV{ja*LrL%Pwm$3t)YLS2U~Y&+z40*Akd8&o=`2*o7h)#dh+5RYp*nUM zf5S((8Mi$&9c}-}%a;x^`aUvmyb@LM9IC-ZkIkCcgxN^%!Gd@fV{t+zd%`J6IxU-7 zi)z_XYWKVf0?e`XG(hNwk895o^{y+pL==AnjU1^VJ{)SMo~Sa=Hc#^+IQbQg8s zYt#t*gK5#{x%pK~22_3s>OrB{1sh^>JcL@b-V86y0}G=%P!ctFwNQ($8S2gYp*k`Z z)$nYaUW6LKm8dtrj1lZZP*1=(@@^e@j?_)g7@yhNV3?W?_6W}P+h)j27{$~@3OU8WEjVrM#z1qa`mH-CX8D9`Yx`4DS}SxHYr_vine zM9z|N2cvQGUyd^#1K*poeh-!+{TF8D0R=voHwyh|zKG1lLX;=|WI9|CTa%8!OL!W$ z;nKhPo`{Y9;W)<7e;NP8M1CNmxm}I{xDNHkXD~B8(;0okKuT{?)UTRj>p~SL(!jtnW&R$7Y@fWs41%F z@wltJ1=bGM2>TF+A>+*24It`^E5j-0e1k4F2bg ztjO_6dk%dRzmmk{*j1>a$n`J#ozl6hnQEhoTycLQTa|RKu6CDaPT~ zsR7s)i{fa^jr&mby-$c}Zqvs%8MU!I>EWokJc?SaZ!j&UOyF_9@dRQC(zQ?xjz(?Y zEvO@VH>Shiu`E8rl$bN2$NdveS!5e~o$*B6?T4C^m8c=yidvK>Q4OC(os3s83EsB( zuhE?nTkgvb{i+`yH3i90=Sdn=$NW(pDdwiV{B%l$o;YEs2S-`QqZZ*3OoqEryX7<{ z!kd^JU!Y##8>Ydyi9PN)k_B~tKAeuh*afd*8LX6q#me)YVMI7%oa3mTZ%=9(+J`y; zFQKO3FZ>15CNpd0KAtBXFS*Crhqv${&P(BO8lg9(S?!mxJZUGD=~xA8O?3BvVFqTHSCkd<9;P;irQY=QH%2}Mqu`|W*g2x?Vc;B`=3`dRlXwL=>qD_9-$7Lcc^nARzdS7*-%5A8`Yr# zSPF|{DvUzC*m%?;UWIzjWAv&=?}=o@)P+nBi(+xojj;kwK^?j0&7`dv;Xy`;bf@6p4I`VIUa_3qxm*}8D=HD4)vh3 zs5yRsS@2KP3#BSz7HbLAVy=j4rxofs;iwKp7GeLZC%wqf0n#7!rfYBj?m-P%k)mdi zMWN<$FzUl(F{+^*sLy~sHvgDSUqe0k0qVieu?fCIog+28#Y_XkP(2@w8q(ER2zOvt z55FwHo!Fp+hi^N4x&@hf=S!JgatGC+*rh$rV#+h%Qqud&m|fDQtQoQXs9iGwwU)fg zh=dc_g<3Qj%b7*f6l0L?fm$2AQA0T$tKde|NWDiLII+r`4pl*oM153wOVkv1LoMck zs6{yu>uLWlBckp00kx`QRxoFH0M;el9(BV`48h+}4@gzf<9tLv+{+uMspN5g|9`Qv z$Nghfr(losJLN^Hn1d%qRgZIxbOzMg`GBc-z7sdZ422)+4U3>ouuxos3sD^ispfGG zVQVage$|bQ@ki2=aW3YrVLn5SVKC{~HQ9c2uoezs`*f*g4y31bSiIW*Ux{dl6NQ>N z%!}$s1yp`Z48=jHDLIXryR)dFe~48vYh5#95vUFxK&_$osIxw2J&*f`j&^vSIeCKK zITY+^<#A49{nj4mPxNbJ&h`{xbcCKaL4DlzZtKpOvjuCAj@i!R{(KOM z7cY@c+}@m|k8nNdWZ~w^>A`UJzdspiJD4-R6zafei`oT|sKqnTrbnaBg&Ee3sI_ns zHBuK)i|#GzT=3~=@{^&qUjfu-MG4fltkBVG7DEUb8q(&dx$cf?Xf$fo&PIKDtwD9* z2tLDGSOQOUGVM5>&G&`mSe^WtsKs^{)uBI8?Z@b1)N8*`(qXT2oX83? z&YKLUe-ATPqp%R-tMtPhtoQ4bW zPt?dx@2!z!|1Bk=kH`I}p}UTnvlpl@4qtITrs!j~)fSvd`YATU=)Pt#T|+IdyEqk} zp{8IUze-QYE}DWWU(ny<^u(1Ito@&6fLVO4P-~!nB#p+2Z>HWqc`GSv3kf?6vFPzT5j)QH?i-S-kJ<2TeBRvuwSu$Hw6YE865wb$QE zL=PH;S#UAx8_rQ%;VJ6id5<|T(MVIj80x`w(Va5POS%iHBePK>w-)uLhf$09H`Ek8 zK)rzXJrTX(XVeYxMw##N{-{ON9M#j2s41C-dcaSpH#~*v$VJSDw^8Rvg3+eDG=`84 zMy;jEm>cKfQSJX@MD(DsW6Yau!SbXJp&Is#H9e1yLrD9h-gptZCmd?q9!2e@i>Nnj zFwS(e1FE9~um}!Ez4w}59?sJDP~Fzpx*oyY6{<@`}e;%Q_WCjM4bnPP(xoE3t$t} zjpI;rJQcOr4qz^Pfm*aFrqZVC1>p1i!{UfTqC8+Ip2sOo* zaVviE64Cx%H{1LiaLjrV^#;GAZoGloo^P#j=9oFmjCyb(o34!NXk%0dI-x!jx?yLG zMlIr}s9oXxKtv<(4b@Prxn`)+phhGMs=;iihKk{Etb%RuB(}iZ^E}SKI0lE|h#$?D zQ{VagW`rpUMty9LSYW=a?!%7S|1lSu-+)HoSSp-FHQ00!zs=&tofu5|$0Z)87v94v z*kCE2ew6=+8mT{*nJICWn>WmeT4Z@pQyGMMfqJMBZ;S5d{~#io!&#^=kz1{2QH$gi zYGh)pFsnW#s=`4>=g_#CyWKiPbrRVJMlHTMCi2j)V3g9<_&Km)KQE=Bjh|9MTMJQ-h5Ls@pU z*-kZ3L)aL#+B=}$d<3e4KVmN2ih1#-&5ygre3<#6wp%4^gthTcT!9fdcP;zh{TyCr z_IEP$r=SqV#0IE0Xo`Ap7t~M>$AUN$HN+=SN9`F@ho7N-Qu>6$@G~C4==J8Hs<6Ry ztiuNT`QMX_Y*d(mS|qzsLwFK3muGDHGHOb0T3?|KBxj>}a0=A@0jPR~QRTs?jyFZU za0IGee=iXY(OA^n&p>Uv`KW_q5o)y_Kz({$!7P|yllhr25c88Biv4kuP3PQfz8f}1 zeTZ#BP1&!g=iNhf%=?mv9-u#6X%5q(rl1O{;ZCR_o{gG{MW_R5Gink3j9P>jQM=_f zYB&9fs-JkP$JvB_co5IxUYxqk9TBfnbGvD%HER1sqULY{*2a0Lsd$EZ;A`s_978(( z4l~y?P$M@RwOHp_ccD6b5%nU^QJ*F6FoizI5x2kP8Nu+NNCT2wnluq1|H9vqJ9@J7_yIK0n(|36QL=Hwn~ zyLtATH;IE492Iqp%20M=j11s1f>v z>bd8XF(v8^bD`e6vb6@bAgB&yoGwBx2PMxp@u5eX>(-e zK+Sz2)Y;t-wN1lOi?%mv8xBIet}4=Ver&Z&(twwi;nLc0#S8-%%aDj9&N6iD(c#JdE=d2l#6X(rm#ckA-eMC)_=YpA$ncp;ONU{NEc!*(wk6g;~J)C(IvWKzKZ3(YKHbA7Nz_HYFp;L zX3m9ns1LbCsLz_y*Vz9Wisxi#=-;ANX@cvf=Xp_cSq$~DSrs*6^-yor0W|`HP>XXc z>O`D|>fmP72s}a!{cG%pA5rH-ckd1Jad{Zkz%BH{$EXL#zG*s?29;j|)#0Y7Bf5=E zhoc@Cff}JD*cMNr&XN4L%z05C{Yg(ijf8gt5k2@i>dhY5^heZ#oZIH?j*Xg%!l>O* z!=~F}OVR^T-y^PKO?-=bPtC8>dSe9jcG+~+=jLxk9Z?Ushj}pd3-iX6@Hy!f=zjBg z$qR6Q%|AR&L+$?_ugn|#j2ekssDtJK=EPX9%^C?ro&7aXt9lSt!mX&e{D4g{;Enk* zI|M^WFT(-&63_794sXr5QtVImzdpU9iD>_yMh)dR)HY1@mzmQL+(0@EH8qLfo3)S* z8;~x9sy_yM<8IVfxBMSW2Zx|OmJefb{DK9r=tuT{QX(Bbn#Ix|v(Uq#sBg2&K6%{# zOg6*c9{2y^!S^5YCzu~lCt<^X&Ejp38o5Z+3q+w-{dm+O+=Ti{x69`5MZNIdf4%0X z(&V4b=X6a}&vs%}e1gFk_{H?R59&=epjP`c%z-{%&3`?U8}){*th2B$>EBTg4E|<* z726+mr0?(&DM#diHGuE1Wk|O}4dD{h1K*%-$m{WO&-P}hgQ_3u{@JMVE2z&BUmqX$ zAWDj=R{%9tp{UQ671#v52Z`tiP7}l2m>+c>1flkOCDhP1x8-4|p^U^7*b`NMG$z9- zHoerk4po1CX%8bGQeNjKk@RHzZA}@|ykS9WZOle_SJaSCLmgCWQFDC&^#Yer z=f?~5$M>j_NbPId3&7t*Yr-A`Qv7hQ2Jmz}PfqQ!q#oYHSs0ki$616wV=?TI+{ek!k-7l&pwlUR++7ksm05hH zQHyUd>f~FC>gZ+EVosCV$32)D;W$QSGip}_rS*F4=hjB9LSm}J+lWV5+ zHVz~|M|vOkH=0XPBlHf{q5K(qoVFN=TGeMU5_4rVYhgC(MRsE+e1p7?(=?OUtlk!x z%ytP!Et-+o0;i(p_z%>O`t!-s9cN%0jN#|w+{5?oPV$32ju z(Vz4()Knbz649IF4)Eds8l9m=Km1@#oz=(v*eroM$>yQ9+fvj}ufs}s1@(sMvzawe z4{MR0h`R4OYIod6ZQGZqqulGsZYm@|RY-xUFcWIK6-CWeZPZ-;fI2|VV-|dg+P;Z% zm{pwzbu^bmJs<>iF4RZW>wxOeaO7O!zyC84XDaFo$1GIC3s7Ga4q;I|hdM%I=QIrm zp{6Prbzfc73pBCmwx~DljM{F|sCFizrr-z`(MEnqL=C6S<>US(v?it`{R^t+w^7^k zE$YoYxy=YA!JMRXU`!0PHbix#8EVAZqaQ9seO4UB3itxuzyIgUW8S!owGHZ^8G)M9 zwWu5Jp{67tuaCZ`IHgfTSr+w%RWJcIK-CMw033*_w-D9Qm8ch6hh7cc0V2BbII7}# zRQ`3$i1$#7?wd7EKC_yWq8ciOI+Ckler$=EaU!bS^{5|6cB9(8Y`v9_{jUc;Bts8) ziF)JrsD|@$>de4$SRF5*8psrAMkpKVL#ie!zddS-y4w7Ks5LU(<}bAA4H!=T?m+f` z2$9SM%+PegT%;pWi*Ggt;%d~8>$`!jZ6xv%CdM%7!PbfzU#uaT@vW)r@job!~oo>V3c7+DIAlv>B&kEb2$$d+Ps9NW_;yhwsYn zHJ*&$ct{|X;vk<$|9fpAHkI=Fgsg;Zq(dn8vG?w=HOGMz;Y#+6{vodG7?nE^=T{=`#gD=6mE6`VN_wivaz5FH`Lgd`dr50^ zHs$`^L&NLvt5r#E9Hpm2b{qKT?hWt!XTU1mAtvct5JUh z;Slw71nAP2Ab-mAL(UW8$!PBgdT=xMl_UJcJq_>_`CJ-t{`$rZg{jnvcxnniQ|VWn zO{nMAF#p_vM3iZ3>smv+CHLv-LmS_(GUV&}8THLeS2|lhllX4(XS->hf0jgD5@~su zuHJ-HH2ORFrT)|CCh~7kCzs6&BfW!ghqNZH67}_Q5sT2y*6BxF*F4I;Up~akQhu3? zSR6y{HI0luDzKe+r$XY>$XiJ~9_1fw*)7uX>EHx=Z%5+8RBqExtu@IXPktonG44~? zyTtiQ<6f~T_x?blIT2lVc)&z%SVbsAIxaRKytg+bCBBUKJHiR-KC}6C?frZga_Up( z0PdvsCe6^PuT{h;O~@qsH-az z4?Skf5|GC?NcZZ-zkFkG{~u%hVzS)-KW2*dqW)N0FO2pR#$f)h+l-@Rlp;QpN~MS& z#`*REf7ymA+c#CR4s{=4e5^^mR@j4}@2k49(8d(f4RIOe;oQ64wx{wv#4C9DM_&Uc zQSmGlb)6x8grI9MAq91QB-EyX9E3>XQMPUqY){@0w>G23eP8Uo`W~B$@}sJ6%S+n( zY|`=mVKWAiGm473{P2v4Iz?#w9~)mz-Gek9Wba9YCn>8#{Q%-=?fpxrGlzIf@;2Dc zG{TCu9e#FpR*|mu{Szm^bbyXQ{>_)O+wGIi~xv5qF{{6pT)HhzbA3~q`= z`8iwn0A(4tw?7uAEYxk3LyJ0m%X3O#4ax&3FHL$T@gC$Qvh`w{^UpltcM396Ar5v# zT^EV#s!Z@9ZyY{Q23KVoJcgeMD=14(NKD>Z(yi|B1V@bkRLQ3j%#-GSv$31~KlXNe+23MO}Yd zhtSUVE5rBm{{flBsj&Ayh0)a7N(1^DSeKBFhw0}NU7ZN}rmkzNy{9$zr?BzO^$@o!0@G?|5{T)@6{A$wCC(r>t_4dpoq5rk^wH6-t=t<#uv zFZICY9e1_$u$@!S7cvb()d)2VpLm3vtSS%4-o%VAGeWd!0~~4lE<@4fRJ5 zZg9^M)v+C`qZ=X#xv4zL{f0GzSbD+;f_^U5HQnH}AnyzDHr(z{Q-5-28r1bGo~Aqr zdGASYz*%^daEbhMw%%XlFDIRW^xuR>q#NK4Q|@(=Q+R~TG=xLMI}y&&$QCNRC9W$M z@lBMsw(p_5=j5d&y%H~xK1uliLP-^J^;LU>YlMc>yN9|SQ2!cXC&5pju&F5=N#R`z zgZ}e?%J$(;?Tx3oA(_ovXjOx{ezJ9*Tb0+{)|spX*9FSb(|&*Q+tQXU{omF&`~F{J z3I5ZGXSTs4ROa^*PJ$Ur8YK62x3{Z_XyVtghZ0$JVb% zeP6}x^PTo2rqe)Ed_lg4_zzT?O6Xv3`j>oNy9ui(?@ZbEt3D6OL7uLwR>d0=c98a` zycq4=v~9)U-i(Cr&v(v}s7=Vn4Wm)lB->D1(rvi0&3_tJIlnA%HmOl1xX#)88j_cU zP=>tf+;g1}mvl~ou6T4NHg&sc{^y%K_xC}H)}qn?ThSIdAvEA6#3L-RnPqLAa2o1B zxKDUXxvqLVpep4Hh=&mGO?(mY)YSQYrE}}@{`+l1w<-LnhHN9j)co;#ZA?l2CdwOO zH0GmWUF`_do$wRh&iN`x$w4X05LAsb;h>AbX)p0dXH zoV-ee!i0w84a7j(-Z09u5t?aLcPE^u;0@s`l^&{q8~%IsqwF~O4{ZZK6JKiMs&|RB zuKLQe`5k!soo=q<45hq0VLsuTt(%)NZx$Y?>mxTUw>OR@Z#j8Oskj}}(9lL(rn*Dz zJ&HFaK8CQu=B2V0qii^#rhS;|{zf=M`lHR;%k!1*T%_^sg#0`xGtQzy9^%#AQoaq5 zUQNSfxmRcY{hhppHrr27)z zuKk~wf@vgjb5nL6R2}vItX$V9(%-MOwy{Oj-$1;yt+R;uSlg*gR4$=7A*n6TVDHP1 zCkfkaUA61IsDHTv$y|o&&?_EV##XvQ-fLChT553r&ePO3Uf<@mrv5h@Z;7`FB?vcZ zZvytl)6{8B+f|9{3e}Ir=g7=K#(pe9W-2Q5B)lYyB>eX(!9$x6UXY&=i*b*x4+iIp zP4C1mgdfO%MQB6&E4U{o@i6X-O*|&$Wnwb_BZ!ozpekM=G$*d>9yi4yU4a|=m@MZl zH_ox~f|P$Eo}4UHW+j1oGs}>d2Cp^lj+PQb9K5e%h*i@sGCu&>V7int7 zE0nKb0lxwzf{GTHou{}blTDW-y{mq4&#b_VmBZUbg!Kw**ZJtSZhq~<{lePuAon@# z!`kn9+QHL1f7;Mq-P=ZlM}|fDwd>x^kEew6Y3n|0SD%@lOjUgRqIS)=;rZO)e*k~f Bdt?9r diff --git a/config/locale/es/app.po b/config/locale/es/app.po index 2c6314d911..01ecd4eeb0 100644 --- a/config/locale/es/app.po +++ b/config/locale/es/app.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: app 1.0\n" "Report-Msgid-Bugs-To: contact@translation.io\n" -"POT-Creation-Date: 2022-02-10 08:50+0000\n" -"PO-Revision-Date: 2022-02-10 09:50+0100\n" +"POT-Creation-Date: 2022-03-10 13:47+0000\n" +"PO-Revision-Date: 2022-03-10 14:48+0100\n" "Last-Translator: FULL NAME \n" "Language-Team: Spanish\n" "Language: es\n" @@ -38,11 +38,11 @@ msgstr "Malas credenciales" msgid "Departments code and name must be unique" msgstr "El código y el nombre del departamento deben ser únicos" -#: ../../app/controllers/api/v0/plans_controller.rb:27 +#: ../../app/controllers/api/v0/plans_controller.rb:28 msgid "user must be in your organisation" msgstr "el usuario debe estar en su organización" -#: ../../app/controllers/api/v0/plans_controller.rb:56 +#: ../../app/controllers/api/v0/plans_controller.rb:57 msgid "Bad Parameters" msgstr "Parámetros erróneos" @@ -208,9 +208,9 @@ msgstr "los locales deberían ser un objeto Hash" msgid "Restricted access to View All the records" msgstr "Acceso restringido a Ver todos los registros." -#: ../../app/controllers/concerns/paginable.rb:182 -msgid "Sort by %s" -msgstr "Ordenar por % s" +#: ../../app/controllers/concerns/paginable.rb:183 +msgid "Sort by %" +msgstr "Ordenar por %" #: ../../app/controllers/concerns/template_methods.rb:8 msgid "customisation" @@ -1924,9 +1924,9 @@ msgstr "" "pplication_name}, complete el siguiente formulario." #: ../../app/views/contact_us/contacts/new.html.erb:10 -#: ../../app/views/layouts/_branding.html.erb:37 -#: ../../app/views/layouts/_footer.html.erb:24 -#: ../../app/views/layouts/_footer.html.erb:30 +#: ../../app/views/layouts/_branding.html.erb:41 +#: ../../app/views/layouts/_footer.html.erb:23 +#: ../../app/views/layouts/_footer.html.erb:29 #: ../../app/views/org_admin/plans/index.html.erb:40 #: ../../app/views/paginable/plans/_organisationally_or_publicly_visible.html.erb:39 #: ../../app/views/paginable/plans/_publicly_visible.html.erb:24 @@ -2459,7 +2459,6 @@ msgid "Do you have a %{application_name} account?" msgstr "¿Tienes una cuenta %{application_name}?" #: ../../app/views/devise/registrations/new.html.erb:22 -#: ../../app/views/layouts/_navigation.html.erb:63 #: ../../app/views/layouts/_signin_signout.html.erb:41 #: ../../app/views/shared/_access_controls.html.erb:5 #: ../../app/views/shared/_sign_in_form.html.erb:19 @@ -2555,7 +2554,7 @@ msgstr "Ver todas las orientaciones" #: ../../app/views/guidances/admin_index.html.erb:5 #: ../../app/views/guidances/new_edit.html.erb:12 #: ../../app/views/guidances/new_edit.html.erb:16 -#: ../../app/views/layouts/_branding.html.erb:77 +#: ../../app/views/layouts/_branding.html.erb:82 #: ../../app/views/org_admin/annotations/_form.html.erb:3 #: ../../app/views/org_admin/questions/_show.html.erb:78 #: ../../app/views/paginable/themes/_index.html.erb:5 @@ -2681,40 +2680,40 @@ msgstr "próximo" msgid "Previous" msgstr "Anterior" -#: ../../app/views/layouts/_branding.html.erb:60 +#: ../../app/views/layouts/_branding.html.erb:65 msgid "Admin" msgstr "Administración" -#: ../../app/views/layouts/_branding.html.erb:66 +#: ../../app/views/layouts/_branding.html.erb:71 #: ../../app/views/org_admin/users/edit.html.erb:71 #: ../../app/views/paginable/users/_index.html.erb:26 #: ../../app/views/super_admin/users/edit.html.erb:105 msgid "Plans" msgstr "Planes" -#: ../../app/views/layouts/_branding.html.erb:72 +#: ../../app/views/layouts/_branding.html.erb:77 #: ../../app/views/org_admin/templates/index.html.erb:4 #: ../../app/views/paginable/orgs/_index.html.erb:8 msgid "Templates" msgstr "Plantillas" -#: ../../app/views/layouts/_branding.html.erb:82 +#: ../../app/views/layouts/_branding.html.erb:87 #: ../../app/views/super_admin/orgs/index.html.erb:2 #: ../../app/views/super_admin/orgs/index.html.erb:6 msgid "Organisations" msgstr "Organizaciones" -#: ../../app/views/layouts/_branding.html.erb:87 +#: ../../app/views/layouts/_branding.html.erb:92 #: ../../app/views/orgs/admin_edit.html.erb:1 #: ../../app/views/orgs/admin_edit.html.erb:6 msgid "Organisation details" msgstr "Detalles de la entidad" -#: ../../app/views/layouts/_branding.html.erb:93 +#: ../../app/views/layouts/_branding.html.erb:98 msgid "Users" msgstr "Usuarios" -#: ../../app/views/layouts/_branding.html.erb:98 +#: ../../app/views/layouts/_branding.html.erb:103 #: ../../app/views/org_admin/questions/_show.html.erb:88 #: ../../app/views/org_admin/shared/_theme_selector.html.erb:16 #: ../../app/views/paginable/guidances/_index.html.erb:7 @@ -2723,15 +2722,15 @@ msgstr "Usuarios" msgid "Themes" msgstr "Temas" -#: ../../app/views/layouts/_branding.html.erb:103 +#: ../../app/views/layouts/_branding.html.erb:108 msgid "Usage" msgstr "Uso" -#: ../../app/views/layouts/_branding.html.erb:108 +#: ../../app/views/layouts/_branding.html.erb:113 msgid "Api Clients" msgstr "Clientes Api" -#: ../../app/views/layouts/_branding.html.erb:111 +#: ../../app/views/layouts/_branding.html.erb:116 #: ../../app/views/org_admin/plans/index.html.erb:11 #: ../../app/views/super_admin/notifications/index.html.erb:1 #: ../../app/views/super_admin/notifications/index.html.erb:4 @@ -2759,11 +2758,7 @@ msgid "Privacy statement" msgstr "Declaracion de privacidad" #: ../../app/views/layouts/_footer.html.erb:17 -msgid "Accessibility statement" -msgstr "Declaración de accesibilidad" - -#: ../../app/views/layouts/_footer.html.erb:18 -msgid "Github" +msgid "GitHub" msgstr "Github" #: ../../app/views/layouts/_navigation.html.erb:12 diff --git a/config/locale/fi/LC_MESSAGES/app.mo b/config/locale/fi/LC_MESSAGES/app.mo index d6273049b123f5ec2b4c0322c1b8f9f1c3e35469..ae9f05e648b9556b6b2006c7bf5b684b7a5d187e 100644 GIT binary patch delta 24352 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 diff --git a/config/locale/fi/app.po b/config/locale/fi/app.po index d691fcfc18..c8bc3f10df 100644 --- a/config/locale/fi/app.po +++ b/config/locale/fi/app.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: app 1.0\n" "Report-Msgid-Bugs-To: contact@translation.io\n" -"POT-Creation-Date: 2022-02-10 08:50+0000\n" -"PO-Revision-Date: 2022-02-10 09:50+0100\n" +"POT-Creation-Date: 2022-03-10 13:47+0000\n" +"PO-Revision-Date: 2022-03-10 14:48+0100\n" "Last-Translator: FULL NAME \n" "Language-Team: Finnish\n" "Language: fi\n" @@ -38,11 +38,11 @@ msgstr "Tunnistetiedot eivät kelpaa" msgid "Departments code and name must be unique" msgstr "Osaston koodin ja nimen on oltava yksilöivä" -#: ../../app/controllers/api/v0/plans_controller.rb:27 +#: ../../app/controllers/api/v0/plans_controller.rb:28 msgid "user must be in your organisation" msgstr "Käyttäjän on oltava organisaatiostasi" -#: ../../app/controllers/api/v0/plans_controller.rb:56 +#: ../../app/controllers/api/v0/plans_controller.rb:57 msgid "Bad Parameters" msgstr "Virheelliset parametrit" @@ -208,9 +208,9 @@ msgstr "paikalliset pitäisi olla Hash objekti" msgid "Restricted access to View All the records" msgstr "Kaikkien tietojen tarkastelu on rajoitettu" -#: ../../app/controllers/concerns/paginable.rb:182 -msgid "Sort by %s" -msgstr "Lajitella jonkun mukaan % s" +#: ../../app/controllers/concerns/paginable.rb:183 +msgid "Sort by %" +msgstr "Lajitella jonkun mukaan %" #: ../../app/controllers/concerns/template_methods.rb:8 msgid "customisation" @@ -1598,20 +1598,22 @@ msgstr "täytyy päästä mallien sovellusliittymään" #: ../../app/presenters/contributor_presenter.rb:42 msgid "Data Manager" -msgstr "Data Manager" +msgstr "Datamanageri" #: ../../app/presenters/contributor_presenter.rb:44 msgid "Project Administrator" -msgstr "Projektin ylläpitäjä" +msgstr "Projektivastaava" #: ../../app/presenters/contributor_presenter.rb:46 msgid "Principal Investigator" msgstr "" "Vastaava tutkija\n" +"\n" +"\n" #: ../../app/presenters/contributor_presenter.rb:48 msgid "Other" -msgstr "Muut" +msgstr "Muu" #: ../../app/presenters/contributor_presenter.rb:56 msgid "" @@ -1898,9 +1900,9 @@ msgstr "" "tä meihin %{application_name} liittyen, täytä alla oleva lomake." #: ../../app/views/contact_us/contacts/new.html.erb:10 -#: ../../app/views/layouts/_branding.html.erb:37 -#: ../../app/views/layouts/_footer.html.erb:24 -#: ../../app/views/layouts/_footer.html.erb:30 +#: ../../app/views/layouts/_branding.html.erb:41 +#: ../../app/views/layouts/_footer.html.erb:23 +#: ../../app/views/layouts/_footer.html.erb:29 #: ../../app/views/org_admin/plans/index.html.erb:40 #: ../../app/views/paginable/plans/_organisationally_or_publicly_visible.html.erb:39 #: ../../app/views/paginable/plans/_publicly_visible.html.erb:24 @@ -2433,7 +2435,6 @@ msgid "Do you have a %{application_name} account?" msgstr "Onko sinulla jo %{application_name} -tili?" #: ../../app/views/devise/registrations/new.html.erb:22 -#: ../../app/views/layouts/_navigation.html.erb:63 #: ../../app/views/layouts/_signin_signout.html.erb:41 #: ../../app/views/shared/_access_controls.html.erb:5 #: ../../app/views/shared/_sign_in_form.html.erb:19 @@ -2528,7 +2529,7 @@ msgstr "Näytä kaikki ohjeistukset" #: ../../app/views/guidances/admin_index.html.erb:5 #: ../../app/views/guidances/new_edit.html.erb:12 #: ../../app/views/guidances/new_edit.html.erb:16 -#: ../../app/views/layouts/_branding.html.erb:77 +#: ../../app/views/layouts/_branding.html.erb:82 #: ../../app/views/org_admin/annotations/_form.html.erb:3 #: ../../app/views/org_admin/questions/_show.html.erb:78 #: ../../app/views/paginable/themes/_index.html.erb:5 @@ -2650,40 +2651,40 @@ msgstr "Seuraava" msgid "Previous" msgstr "Edellinen" -#: ../../app/views/layouts/_branding.html.erb:60 +#: ../../app/views/layouts/_branding.html.erb:65 msgid "Admin" msgstr "Ylläpitäjä" -#: ../../app/views/layouts/_branding.html.erb:66 +#: ../../app/views/layouts/_branding.html.erb:71 #: ../../app/views/org_admin/users/edit.html.erb:71 #: ../../app/views/paginable/users/_index.html.erb:26 #: ../../app/views/super_admin/users/edit.html.erb:105 msgid "Plans" msgstr "Suunnitelmaa" -#: ../../app/views/layouts/_branding.html.erb:72 +#: ../../app/views/layouts/_branding.html.erb:77 #: ../../app/views/org_admin/templates/index.html.erb:4 #: ../../app/views/paginable/orgs/_index.html.erb:8 msgid "Templates" msgstr "Suunnitelmapohjat" -#: ../../app/views/layouts/_branding.html.erb:82 +#: ../../app/views/layouts/_branding.html.erb:87 #: ../../app/views/super_admin/orgs/index.html.erb:2 #: ../../app/views/super_admin/orgs/index.html.erb:6 msgid "Organisations" msgstr "Organisaatiota" -#: ../../app/views/layouts/_branding.html.erb:87 +#: ../../app/views/layouts/_branding.html.erb:92 #: ../../app/views/orgs/admin_edit.html.erb:1 #: ../../app/views/orgs/admin_edit.html.erb:6 msgid "Organisation details" msgstr "Organisaation tiedot" -#: ../../app/views/layouts/_branding.html.erb:93 +#: ../../app/views/layouts/_branding.html.erb:98 msgid "Users" msgstr "Käyttäjää" -#: ../../app/views/layouts/_branding.html.erb:98 +#: ../../app/views/layouts/_branding.html.erb:103 #: ../../app/views/org_admin/questions/_show.html.erb:88 #: ../../app/views/org_admin/shared/_theme_selector.html.erb:16 #: ../../app/views/paginable/guidances/_index.html.erb:7 @@ -2692,15 +2693,15 @@ msgstr "Käyttäjää" msgid "Themes" msgstr "Teemat" -#: ../../app/views/layouts/_branding.html.erb:103 +#: ../../app/views/layouts/_branding.html.erb:108 msgid "Usage" msgstr "Käyttö" -#: ../../app/views/layouts/_branding.html.erb:108 +#: ../../app/views/layouts/_branding.html.erb:113 msgid "Api Clients" msgstr "Api-asiakkaat" -#: ../../app/views/layouts/_branding.html.erb:111 +#: ../../app/views/layouts/_branding.html.erb:116 #: ../../app/views/org_admin/plans/index.html.erb:11 #: ../../app/views/super_admin/notifications/index.html.erb:1 #: ../../app/views/super_admin/notifications/index.html.erb:4 @@ -2728,11 +2729,7 @@ msgid "Privacy statement" msgstr "Tietosuojaseloste" #: ../../app/views/layouts/_footer.html.erb:17 -msgid "Accessibility statement" -msgstr "Saavutettavuusseloste (englanniksi)" - -#: ../../app/views/layouts/_footer.html.erb:18 -msgid "Github" +msgid "GitHub" msgstr "GitHub" #: ../../app/views/layouts/_navigation.html.erb:12 diff --git a/config/locale/fr_CA/LC_MESSAGES/app.mo b/config/locale/fr_CA/LC_MESSAGES/app.mo index 03cdd7a0b12b4582aead31559f3191887f7f9e24..c99a8d85fd717c4c1af1d0a0b54d08275ada0dec 100644 GIT binary patch delta 21181 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 Qh2Khyn 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 UuPpU;-u{@f_TI;}Tp{!R4\n" "Language-Team: French\n" "Language: fr_CA\n" @@ -36,11 +36,11 @@ msgstr "Mauvais renseignements d’identification" msgid "Departments code and name must be unique" msgstr "Le code et le nom du département doivent être uniques" -#: ../../app/controllers/api/v0/plans_controller.rb:27 +#: ../../app/controllers/api/v0/plans_controller.rb:28 msgid "user must be in your organisation" msgstr "l’utilisateur doit faire partie de votre organisme" -#: ../../app/controllers/api/v0/plans_controller.rb:56 +#: ../../app/controllers/api/v0/plans_controller.rb:57 msgid "Bad Parameters" msgstr "Mauvais paramètres" @@ -204,8 +204,8 @@ msgstr "les valeurs locales doivent être un objet Hash" msgid "Restricted access to View All the records" msgstr "Accès restreint à Voir Tous les enregistrements" -#: ../../app/controllers/concerns/paginable.rb:182 -msgid "Sort by %s" +#: ../../app/controllers/concerns/paginable.rb:183 +msgid "Sort by %" msgstr "" #: ../../app/controllers/concerns/template_methods.rb:8 @@ -1896,9 +1896,9 @@ msgstr "" "me}, veuillez remplir le formulaire ci-dessous." #: ../../app/views/contact_us/contacts/new.html.erb:10 -#: ../../app/views/layouts/_branding.html.erb:37 -#: ../../app/views/layouts/_footer.html.erb:24 -#: ../../app/views/layouts/_footer.html.erb:30 +#: ../../app/views/layouts/_branding.html.erb:41 +#: ../../app/views/layouts/_footer.html.erb:23 +#: ../../app/views/layouts/_footer.html.erb:29 #: ../../app/views/org_admin/plans/index.html.erb:40 #: ../../app/views/paginable/plans/_organisationally_or_publicly_visible.html.erb:39 #: ../../app/views/paginable/plans/_publicly_visible.html.erb:24 @@ -2432,7 +2432,6 @@ msgid "Do you have a %{application_name} account?" msgstr "Avez-vous un compte %{application_name}?" #: ../../app/views/devise/registrations/new.html.erb:22 -#: ../../app/views/layouts/_navigation.html.erb:63 #: ../../app/views/layouts/_signin_signout.html.erb:41 #: ../../app/views/shared/_access_controls.html.erb:5 #: ../../app/views/shared/_sign_in_form.html.erb:19 @@ -2529,7 +2528,7 @@ msgstr "Voir toutes les directives" #: ../../app/views/guidances/admin_index.html.erb:5 #: ../../app/views/guidances/new_edit.html.erb:12 #: ../../app/views/guidances/new_edit.html.erb:16 -#: ../../app/views/layouts/_branding.html.erb:77 +#: ../../app/views/layouts/_branding.html.erb:82 #: ../../app/views/org_admin/annotations/_form.html.erb:3 #: ../../app/views/org_admin/questions/_show.html.erb:78 #: ../../app/views/paginable/themes/_index.html.erb:5 @@ -2658,40 +2657,40 @@ msgstr "Suivant" msgid "Previous" msgstr "Précédent" -#: ../../app/views/layouts/_branding.html.erb:60 +#: ../../app/views/layouts/_branding.html.erb:65 msgid "Admin" msgstr "Admin" -#: ../../app/views/layouts/_branding.html.erb:66 +#: ../../app/views/layouts/_branding.html.erb:71 #: ../../app/views/org_admin/users/edit.html.erb:71 #: ../../app/views/paginable/users/_index.html.erb:26 #: ../../app/views/super_admin/users/edit.html.erb:105 msgid "Plans" msgstr "Plans" -#: ../../app/views/layouts/_branding.html.erb:72 +#: ../../app/views/layouts/_branding.html.erb:77 #: ../../app/views/org_admin/templates/index.html.erb:4 #: ../../app/views/paginable/orgs/_index.html.erb:8 msgid "Templates" msgstr "Modèles" -#: ../../app/views/layouts/_branding.html.erb:82 +#: ../../app/views/layouts/_branding.html.erb:87 #: ../../app/views/super_admin/orgs/index.html.erb:2 #: ../../app/views/super_admin/orgs/index.html.erb:6 msgid "Organisations" msgstr "Organismes" -#: ../../app/views/layouts/_branding.html.erb:87 +#: ../../app/views/layouts/_branding.html.erb:92 #: ../../app/views/orgs/admin_edit.html.erb:1 #: ../../app/views/orgs/admin_edit.html.erb:6 msgid "Organisation details" msgstr "Renseignements sur l’organisme" -#: ../../app/views/layouts/_branding.html.erb:93 +#: ../../app/views/layouts/_branding.html.erb:98 msgid "Users" msgstr "Utilisateurs" -#: ../../app/views/layouts/_branding.html.erb:98 +#: ../../app/views/layouts/_branding.html.erb:103 #: ../../app/views/org_admin/questions/_show.html.erb:88 #: ../../app/views/org_admin/shared/_theme_selector.html.erb:16 #: ../../app/views/paginable/guidances/_index.html.erb:7 @@ -2700,15 +2699,15 @@ msgstr "Utilisateurs" msgid "Themes" msgstr "Thèmes" -#: ../../app/views/layouts/_branding.html.erb:103 +#: ../../app/views/layouts/_branding.html.erb:108 msgid "Usage" msgstr "Utilisation" -#: ../../app/views/layouts/_branding.html.erb:108 +#: ../../app/views/layouts/_branding.html.erb:113 msgid "Api Clients" msgstr "Clients API" -#: ../../app/views/layouts/_branding.html.erb:111 +#: ../../app/views/layouts/_branding.html.erb:116 #: ../../app/views/org_admin/plans/index.html.erb:11 #: ../../app/views/super_admin/notifications/index.html.erb:1 #: ../../app/views/super_admin/notifications/index.html.erb:4 @@ -2736,12 +2735,8 @@ msgid "Privacy statement" msgstr "Déclaration de confidentialité" #: ../../app/views/layouts/_footer.html.erb:17 -msgid "Accessibility statement" -msgstr "" - -#: ../../app/views/layouts/_footer.html.erb:18 -msgid "Github" -msgstr "" +msgid "GitHub" +msgstr "GitHub" #: ../../app/views/layouts/_navigation.html.erb:12 #: ../../app/views/orgs/_profile_form.html.erb:55 diff --git a/config/locale/fr_FR/LC_MESSAGES/app.mo b/config/locale/fr_FR/LC_MESSAGES/app.mo index aa66bc7601a3aac9a0d552a550d7c0c5dad86578..15a9a5ce7b9bdacded6d19d1407eda75da0e6962 100644 GIT binary patch delta 30972 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 delta 31940 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\n" "Language-Team: French\n" "Language: fr_FR\n" @@ -23,14 +23,14 @@ msgid "" "There is no question with id %s associated to plan id %s" " for which to create or update an answer" msgstr "" -"Il n'y a pas de question avec id % s associés à l'identifiant du " -"plan % s pour lesquels créer ou mettre à jour une réponse" +"Il n'y a pas de question avec l'identifiant %s associé au plan %<" +"plan_id>s pour lequel créer ou mettre à jour une réponse" #: ../../app/controllers/answers_controller.rb:33 msgid "There is no plan with id %s for which to create or update an answer" msgstr "" -"Il n'y a pas de plan avec l'identifiant % s pour lesquels créer ou mettre " -"à jour une réponse" +"Il n'y a pas de plan avec l'identifiant %s pour lesquels créer ou mettre à" +" jour une réponse" #: ../../app/controllers/api/v0/base_controller.rb:126 msgid "Bad Credentials" @@ -40,11 +40,11 @@ msgstr "Mauvaises informations d'authentification" msgid "Departments code and name must be unique" msgstr "Le code et le nom du département doivent être uniques" -#: ../../app/controllers/api/v0/plans_controller.rb:27 +#: ../../app/controllers/api/v0/plans_controller.rb:28 msgid "user must be in your organisation" msgstr "l'utilisateur doit appartenir à votre organisme" -#: ../../app/controllers/api/v0/plans_controller.rb:56 +#: ../../app/controllers/api/v0/plans_controller.rb:57 msgid "Bad Parameters" msgstr "Paramètres erronés" @@ -59,7 +59,7 @@ msgstr "Mois" #: ../../app/controllers/api/v0/statistics_controller.rb:41 #: ../../app/controllers/usage_controller.rb:66 msgid "No. Users joined" -msgstr "Nbre d'utilisateurs inscrits" +msgstr "Nombre d'utilisateurs inscrits" #: ../../app/controllers/api/v0/statistics_controller.rb:47 #: ../../app/controllers/api/v0/statistics_controller.rb:95 @@ -76,19 +76,19 @@ msgstr "users_joined" #: ../../app/controllers/api/v0/statistics_controller.rb:89 #: ../../app/controllers/usage_controller.rb:87 msgid "No. Completed Plans" -msgstr "Non. Plans terminés" +msgstr "Nombre de plans terminés" #: ../../app/controllers/api/v0/statistics_controller.rb:96 msgid "completed_plans" -msgstr "complété_plans" +msgstr "completed_plans" #: ../../app/controllers/api/v0/statistics_controller.rb:136 msgid "No. Plans" -msgstr "Non. Plans" +msgstr "Nombre de plans" #: ../../app/controllers/api/v0/statistics_controller.rb:143 msgid "plans" -msgstr "Des plans" +msgstr "plans" #: ../../app/controllers/api/v1/authentication_controller.rb:47 msgid "Missing or invalid JSON" @@ -104,7 +104,7 @@ msgstr "Plan non trouvé" #: ../../app/controllers/api/v1/plans_controller.rb:36 msgid "Unable to create your DMP" -msgstr "Impossible de créer votre DMP" +msgstr "Impossible de créer votre plan" #: ../../app/controllers/api/v1/plans_controller.rb:37 msgid "Plan already exists. Send an update instead." @@ -115,12 +115,12 @@ msgid "" "Could not determine ownership of the DMP. Please add an\n" " :affiliation to the :contact" msgstr "" -"Impossible de déterminer la propriété du DMP. Veuillez ajouter un\n" -" : affiliation au: contact" +"Impossible de déterminer la propriété du plan. Veuillez ajouter une\n" +" :affiliation au :contact" #: ../../app/controllers/api/v1/plans_controller.rb:70 msgid "Invalid JSON!" -msgstr "JSON invalide!" +msgstr "JSON invalide !" #: ../../app/controllers/api/v1/plans_controller.rb:73 msgid "Invalid JSON" @@ -140,11 +140,11 @@ msgstr "Vous devez vous connecter ou vous enregistrer afin de continuer." #: ../../app/controllers/application_controller.rb:108 msgid "Unable to %s the %s.%s" -msgstr "Incapable de % c'est le % s. % s" +msgstr "Impossible de % le %s. %s" #: ../../app/controllers/application_controller.rb:114 msgid "Successfully %s the %s." -msgstr "Avec succès % c'est le % s." +msgstr "%s %s avec succès." #: ../../app/controllers/application_controller.rb:127 msgid "API client" @@ -164,7 +164,7 @@ msgstr "Commentaire" #: ../../app/controllers/application_controller.rb:131 msgid "organisation" -msgstr "organisation" +msgstr "organisme" #: ../../app/controllers/application_controller.rb:132 msgid "permission" @@ -210,9 +210,9 @@ msgstr "locals doit être un objet de type Hash" msgid "Restricted access to View All the records" msgstr "Accès restreint à Voir tous les enregistrements" -#: ../../app/controllers/concerns/paginable.rb:182 -msgid "Sort by %s" -msgstr "Trier par % s" +#: ../../app/controllers/concerns/paginable.rb:183 +msgid "Sort by %" +msgstr "Trier par %" #: ../../app/controllers/concerns/template_methods.rb:8 msgid "customisation" @@ -254,11 +254,11 @@ msgstr "Impossible de soumettre votre demande" #: ../../app/controllers/contributors_controller.rb:40 #: ../../app/controllers/contributors_controller.rb:56 msgid "add" -msgstr "Ajouter" +msgstr "ajouter" #: ../../app/controllers/contributors_controller.rb:54 msgid "added" -msgstr "Ajoutée" +msgstr "ajouté" #: ../../app/controllers/contributors_controller.rb:72 #: ../../app/controllers/guidance_groups_controller.rb:51 @@ -294,7 +294,7 @@ msgstr "enregistré" #: ../../app/controllers/registrations_controller.rb:256 #: ../../app/controllers/registrations_controller.rb:287 msgid "save" -msgstr "Enregistrer" +msgstr "enregistrer" #: ../../app/controllers/contributors_controller.rb:84 #: ../../app/controllers/notes_controller.rb:131 @@ -308,7 +308,7 @@ msgstr "supprimé(e)" #: ../../app/controllers/org_admin/templates_controller.rb:239 #: ../../app/controllers/super_admin/orgs_controller.rb:97 msgid "remove" -msgstr "Supprimer" +msgstr "supprimer" #: ../../app/controllers/contributors_controller.rb:161 msgid "plan not found" @@ -316,7 +316,7 @@ msgstr "Plan non trouvé" #: ../../app/controllers/contributors_controller.rb:169 msgid "contributor not found" -msgstr "contributeur introuvable" +msgstr "contributeur non trouvé" #: ../../app/controllers/feedback_requests_controller.rb:9 msgid "Unable to submit your request for feedback at this time." @@ -356,7 +356,7 @@ msgstr "créé" #: ../../app/controllers/super_admin/orgs_controller.rb:64 #: ../../app/controllers/super_admin/themes_controller.rb:24 msgid "create" -msgstr "Créer" +msgstr "créer" #: ../../app/controllers/guidance_groups_controller.rb:65 msgid "Your guidance group has been published and is now available to users." @@ -378,7 +378,7 @@ msgstr "" #: ../../app/controllers/guidance_groups_controller.rb:81 #: ../../app/controllers/guidances_controller.rb:129 msgid "unpublish" -msgstr "annuler la publication" +msgstr "dépublier" #: ../../app/controllers/guidance_groups_controller.rb:91 #: ../../app/controllers/guidances_controller.rb:93 @@ -406,7 +406,7 @@ msgstr "supprimé" #: ../../app/controllers/super_admin/notifications_controller.rb:88 #: ../../app/controllers/super_admin/themes_controller.rb:54 msgid "delete" -msgstr "Effacer" +msgstr "effacer" #: ../../app/controllers/guidances_controller.rb:109 msgid "Your guidance has been published and is now available to users." @@ -420,11 +420,11 @@ msgstr "" #: ../../app/controllers/identifiers_controller.rb:19 msgid "Successfully unlinked your account from %s." -msgstr "Votre compte a bien été dissocié de % s." +msgstr "Compte délié de %s avec succès." #: ../../app/controllers/identifiers_controller.rb:22 msgid "Unable to unlink your account from %s." -msgstr "Impossible de dissocier votre compte de % s." +msgstr "Impossible de délier votre compte de %s." #: ../../app/controllers/org_admin/phases_controller.rb:17 #: ../../app/controllers/org_admin/templates_controller.rb:115 @@ -437,7 +437,7 @@ msgstr "" #: ../../app/controllers/org_admin/phases_controller.rb:105 msgid "You cannot add a phase to a historical version of a template." -msgstr "Vous ne pouvez pas ajouter de phase à une version historique d'un modèle." +msgstr "Vous ne pouvez pas ajouter de phase à une ancienne version d'un modèle." #: ../../app/controllers/org_admin/phases_controller.rb:126 msgid "Unable to create a new version of this template.
    " @@ -464,7 +464,7 @@ msgstr "mis à jour" #: ../../app/controllers/super_admin/themes_controller.rb:41 #: ../../app/controllers/super_admin/users_controller.rb:56 msgid "update" -msgstr "Mise à jour" +msgstr "mettre à jour" #: ../../app/controllers/org_admin/phases_controller.rb:152 #: ../../app/controllers/org_admin/phases_controller.rb:184 @@ -477,9 +477,7 @@ msgstr "Impossible de créer une nouvelle version de ce modèle." #: ../../app/controllers/org_admin/plans_controller.rb:36 msgid "%s has been notified that you have finished providing feedback" -msgstr "" -"% s a été informé que vous avez terminé de fournir des commentaire" -"s" +msgstr "%s a été informé que vous avez finalisé l'assistance conseil" #: ../../app/controllers/org_admin/plans_controller.rb:40 msgid "Unable to notify user that you have finished providing feedback." @@ -517,15 +515,15 @@ msgstr "Template" #: ../../app/views/shared/org_selectors/_external_only.html.erb:9 #: ../../app/views/shared/org_selectors/_local_only.html.erb:7 msgid "Organisation" -msgstr "Organisation" +msgstr "Organisme" #: ../../app/controllers/org_admin/plans_controller.rb:59 msgid "Owner name" -msgstr "Le nom du propriétaire" +msgstr "Nom du propriétaire" #: ../../app/controllers/org_admin/plans_controller.rb:60 msgid "Owner email" -msgstr "E-mail du propriétaire" +msgstr "Courriel du propriétaire" #: ../../app/controllers/org_admin/plans_controller.rb:61 #: ../../app/views/paginable/plans/_index.html.erb:9 @@ -556,7 +554,7 @@ msgstr "Impossible de supprimer cette version du modèle." #: ../../app/controllers/org_admin/template_copies_controller.rb:20 #: ../../app/controllers/plans_controller.rb:396 msgid "copy" -msgstr "copie" +msgstr "copier" #: #: ../../app/controllers/org_admin/template_customization_transfers_controller.rb:28 @@ -578,7 +576,7 @@ msgstr "Tous les modèles" #: ../../app/controllers/org_admin/templates_controller.rb:47 msgid "%s Templates" -msgstr "% Modèles" +msgstr "Modèles %s " #: ../../app/controllers/org_admin/templates_controller.rb:49 #: ../../app/views/org_admin/templates/index.html.erb:54 @@ -591,16 +589,14 @@ msgstr "Modèles personnalisables" #: ../../app/controllers/org_admin/templates_controller.rb:215 msgid "Error parsing links for a %