From b52f7351c3af4b9b1d7d777eb14d8f06369df30a Mon Sep 17 00:00:00 2001 From: briri Date: Tue, 3 Aug 2021 09:18:28 -0700 Subject: [PATCH 01/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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 379f70d831fe117cf162f9fdf01491deeb506d8e Mon Sep 17 00:00:00 2001 From: Nicolas Franck Date: Tue, 29 Mar 2022 08:55:17 +0200 Subject: [PATCH 11/15] add validation to new madmp models --- Gemfile | 2 + Gemfile.lock | 13 ++++ app/models/license.rb | 16 +++++ app/models/metadata_standard.rb | 59 ++++++++++++++++++ app/models/repository.rb | 102 ++++++++++++++++++++++++++++++++ 5 files changed, 192 insertions(+) diff --git a/Gemfile b/Gemfile index ce079fdbaa..06c6fabd24 100644 --- a/Gemfile +++ b/Gemfile @@ -213,6 +213,8 @@ gem 'httparty' # Autoload dotenv in Rails. (https://github.com/bkeepers/dotenv) gem 'dotenv-rails' +gem 'activerecord_json_validator' + # ================================= # # ENVIRONMENT SPECIFIC DEPENDENCIES # # ================================= # diff --git a/Gemfile.lock b/Gemfile.lock index d1a8d1dba1..0fdc757d62 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,6 +33,9 @@ GEM activemodel (= 5.2.6.2) activesupport (= 5.2.6.2) arel (>= 9.0) + activerecord_json_validator (2.1.0) + activerecord (>= 4.2.0, < 8) + json_schemer (~> 0.2.18) activestorage (5.2.6.2) actionpack (= 5.2.6.2) activerecord (= 5.2.6.2) @@ -130,6 +133,8 @@ GEM dragonfly-s3_data_store (1.3.0) dragonfly (~> 1.0) fog-aws + ecma-re-validator (0.4.0) + regexp_parser (~> 2.2) erubi (1.10.0) excon (0.91.0) execjs (2.8.1) @@ -207,6 +212,7 @@ GEM guard (~> 2.1) guard-compat (~> 1.1) rspec (>= 2.99.0, < 4.0) + hana (1.3.7) hashdiff (1.0.1) hashie (5.0.0) highline (2.0.3) @@ -223,6 +229,11 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) json (2.6.1) + json_schemer (0.2.19) + ecma-re-validator (~> 0.3) + hana (~> 1.3) + regexp_parser (~> 2.0) + uri_template (~> 0.7) jwt (2.3.0) kaminari (1.2.2) activesupport (>= 4.1.0) @@ -474,6 +485,7 @@ GEM thread_safe (~> 0.1) unicode-display_width (2.1.0) uniform_notifier (1.14.2) + uri_template (0.7.0) warden (1.2.9) rack (>= 2.0.9) web-console (3.7.0) @@ -515,6 +527,7 @@ PLATFORMS x86_64-linux DEPENDENCIES + activerecord_json_validator annotate annotate_gem api-pagination diff --git a/app/models/license.rb b/app/models/license.rb index 0c5cd49c3e..c7579624a4 100644 --- a/app/models/license.rb +++ b/app/models/license.rb @@ -49,4 +49,20 @@ class License < ApplicationRecord # Remove any preferred licenses that could not be found in the table licenses.compact } + + # varchar(255) NOT NULL + validates :name, + presence: { message: PRESENCE_MESSAGE }, + length: { in: 0..255, allow_nil: false } + + # varchar(255) NOT NULL + validates :identifier, + presence: { message: PRESENCE_MESSAGE }, + length: { in: 0..255, allow_nil: false } + + # varchar(255) NOT NULL + validates :uri, + presence: { message: PRESENCE_MESSAGE }, + length: { in: 0..255, allow_nil: false } + end diff --git a/app/models/metadata_standard.rb b/app/models/metadata_standard.rb index ea8f69c065..ef795f76cc 100644 --- a/app/models/metadata_standard.rb +++ b/app/models/metadata_standard.rb @@ -15,6 +15,36 @@ # rdamsc_id :string # class MetadataStandard < ApplicationRecord + + # ============= + # = Constants = + # ============= + + # keep "=>" syntax as json_schemer requires string keys + SCHEMA_RELATED_ENTITIES = { + "$schema" => "http://json-schema.org/draft-04/schema#", + "type" => "array", + "items" => { + "type" => "object", + "properties" => { + "id" => { "type" => "string" }, + "role" => { "type" => "string" } + } + } + } + + SCHEMA_LOCATIONS = { + "$schema" => "http://json-schema.org/draft-04/schema#", + "type" => "array", + "items" => { + "type" => "object", + "properties" => { + "url" => { "type" => "string" }, + "type" => { "type" => "string" } + } + } + } + # ================ # = Associations = # ================ @@ -29,4 +59,33 @@ class MetadataStandard < ApplicationRecord term = term.downcase where('LOWER(title) LIKE ?', "%#{term}%").or(where('LOWER(description) LIKE ?', "%#{term}%")) } + + # varchar(255) DEFAULT NULL + validates :title, + length: { maximum: 255 } + + # varchar(255) DEFAULT NULL + validates :rdamsc_id, + length: { maximum: 255 } + + # varchar(255) DEFAULT NULL + validates :uri, + length: { maximum: 255 } + + # json DEFAULT NULL + validates :related_entities, + json: { + schema: SCHEMA_RELATED_ENTITIES, + message: ->(errors) { errors } + }, + allow_nil: true + + # json DEFAULT NULL + validates :locations, + json: { + schema: SCHEMA_LOCATIONS, + message: ->(errors) { errors } + }, + allow_nil: true + end diff --git a/app/models/repository.rb b/app/models/repository.rb index 5db15716eb..e4e52a886a 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -22,6 +22,73 @@ # class Repository < ApplicationRecord + + # ============= + # = Constants = + # ============= + + # keep "=>" syntax as json_schemer requires string keys + SCHEMA_INFO = { + "$schema" => "http://json-schema.org/draft-04/schema#", + "type" => "object", + "properties" => { + "types" => { + "type" => "array", + "items" => { + "type" => "string" + } + }, + "keywords" => { + "type" => "array", + "items" => { + "type" => "string" + } + }, + "subjects" => { + "type" => "array", + "items" => { + "type" => "string" + } + }, + "access" => { + "type" => "string", + "enum" => ["open", "restricted", "closed"] + }, + "provider_types" => { + "type" => "array", + "items" => { + "type" => "string" + } + }, + "upload_types" => { + "type" => "array", + "items" => { + "type" => "object", + "properties" => { + "type" => { "type" => "string" }, + "restriction" => { "type" => "string" } + }, + "required" => ["type","restriction"] + } + }, + "policies" => { + "type" => "array", + "items" => { + "type" => "object", + "properties" => { + "name" => { "type" => "string" }, + "url" => { "type" => "string" } + }, + "required" => ["name","url"] + } + }, + "pid_system" => { + "type" => "string" + } + } + } + + # ================ # = Associations = # ================ @@ -51,4 +118,39 @@ class Repository < ApplicationRecord scope :by_facet, lambda { |facet| where(safe_json_where_clause(column: 'info', hash_key: 'keywords'), "%#{facet}%") } + + # =============== + # = Validations = + # =============== + + # varchar(255) NOT NULL + validates :name, + presence: { message: PRESENCE_MESSAGE }, + length: { in: 0..255, allow_nil: false } + + # text NOT NULL + validates :description, + presence: { message: PRESENCE_MESSAGE } + + # varchar(255) NOT NULL + validates :uri, + presence: { message: PRESENCE_MESSAGE }, + length: { in: 0..255, allow_nil: false } + + # varchar(255) DEFAULT NULL + validates :homepage, + length: { maximum: 255 } + + # varchar(255) DEFAULT NULL + validates :contact, + length: { maximum: 255 } + + # json DEFAULT NULL + validates :info, + json: { + schema: SCHEMA_INFO, + message: ->(errors) { errors } + }, + allow_nil: true + end From a7a2d83c2af899e48b85bc397c4e35d7e91bc511 Mon Sep 17 00:00:00 2001 From: briri Date: Thu, 31 Mar 2022 11:29:07 -0700 Subject: [PATCH 12/15] added node_modules to ignored source paths for translation.io ... should speed up scripts. Reverted some use of '%s' to the preferred '%{var}' style --- app/models/license.rb | 4 ++-- config/initializers/_dmproadmap.rb | 14 +++++++------- config/initializers/translation.rb | 3 ++- spec/models/license_spec.rb | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/models/license.rb b/app/models/license.rb index 0c5cd49c3e..858b1360c9 100644 --- a/app/models/license.rb +++ b/app/models/license.rb @@ -41,9 +41,9 @@ class License < ApplicationRecord licenses = preferences.map do |preference| # If `%{latest}` was specified then grab the most current version - pref = preference.gsub('%s', '[0-9\\.]+$') + pref = preference.gsub('%{latest}', '[0-9\\.]+$') where_clause = safe_regexp_where_clause(column: 'identifier') - rslts = preference.include?('%s') ? where(where_clause, pref) : where(identifier: pref) + 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 diff --git a/config/initializers/_dmproadmap.rb b/config/initializers/_dmproadmap.rb index 7369d3811c..af20b0668d 100644 --- a/config/initializers/_dmproadmap.rb +++ b/config/initializers/_dmproadmap.rb @@ -241,13 +241,13 @@ class Application < Rails::Application # 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' + 'CC-BY-%{latest}s', + 'CC-BY-SA-%{latest}s', + 'CC-BY-NC-%{latest}s', + 'CC-BY-NC-SA-%{latest}s', + 'CC-BY-ND-%{latest}s', + 'CC-BY-NC-ND-%{latest}s', + 'CC0-%{latest}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 diff --git a/config/initializers/translation.rb b/config/initializers/translation.rb index 346da2d33b..3fd5ac5394 100644 --- a/config/initializers/translation.rb +++ b/config/initializers/translation.rb @@ -16,7 +16,7 @@ config.target_locales = %w[de en-GB en-US es fr-FR fi sv-FI pt-BR en-CA fr-CA tr-TR] config.text_domain = 'app' config.bound_text_domains = %w[app client] - config.ignored_source_paths = ['app/views/branded/'] + config.ignored_source_paths = ['app/views/branded/', 'node_modules/'] config.locales_path = Rails.root.join('config', 'locale') end elsif ENV['DOMAIN'] == 'client' @@ -38,6 +38,7 @@ def ignore_paths Dir.glob('**/*').select { |f| File.directory? f } .collect { |name| "#{name}/" } - ['app/', + 'node_modules/', 'app/views/', 'app/views/branded/', 'app/views/branded/public_pages/', diff --git a/spec/models/license_spec.rb b/spec/models/license_spec.rb index f51785ad22..d96544922d 100644 --- a/spec/models/license_spec.rb +++ b/spec/models/license_spec.rb @@ -35,7 +35,7 @@ Rails.configuration.x.madmp.preferred_licenses = [ @preferred_license.identifier, - "#{@preferred_oldest.identifier}-%s" + "#{@preferred_oldest.identifier}-%{latest}s" ] end From 306a6081e2ba3ddd44af38b90ecb6dd30ce5a897 Mon Sep 17 00:00:00 2001 From: briri Date: Thu, 31 Mar 2022 11:39:13 -0700 Subject: [PATCH 13/15] fixed typo in test --- spec/models/license_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/license_spec.rb b/spec/models/license_spec.rb index d96544922d..223ed2f671 100644 --- a/spec/models/license_spec.rb +++ b/spec/models/license_spec.rb @@ -35,7 +35,7 @@ Rails.configuration.x.madmp.preferred_licenses = [ @preferred_license.identifier, - "#{@preferred_oldest.identifier}-%{latest}s" + "#{@preferred_oldest.identifier}-%{latest}" ] end From 8015bb1c0e19e46f3a9730d3d7d6b1583106152c Mon Sep 17 00:00:00 2001 From: briri Date: Thu, 31 Mar 2022 12:49:27 -0700 Subject: [PATCH 14/15] fixed rubocop complaints --- app/models/license.rb | 13 ++-- app/models/metadata_standard.rb | 60 +++++++++-------- app/models/repository.rb | 111 ++++++++++++++++---------------- 3 files changed, 89 insertions(+), 95 deletions(-) diff --git a/app/models/license.rb b/app/models/license.rb index aa09cdd144..e59f74250d 100644 --- a/app/models/license.rb +++ b/app/models/license.rb @@ -52,17 +52,16 @@ class License < ApplicationRecord # varchar(255) NOT NULL validates :name, - presence: { message: PRESENCE_MESSAGE }, - length: { in: 0..255, allow_nil: false } + presence: { message: PRESENCE_MESSAGE }, + length: { in: 0..255, allow_nil: false } # varchar(255) NOT NULL validates :identifier, - presence: { message: PRESENCE_MESSAGE }, - length: { in: 0..255, allow_nil: false } + presence: { message: PRESENCE_MESSAGE }, + length: { in: 0..255, allow_nil: false } # varchar(255) NOT NULL validates :uri, - presence: { message: PRESENCE_MESSAGE }, - length: { in: 0..255, allow_nil: false } - + presence: { message: PRESENCE_MESSAGE }, + length: { in: 0..255, allow_nil: false } end diff --git a/app/models/metadata_standard.rb b/app/models/metadata_standard.rb index ef795f76cc..02eaccfd1f 100644 --- a/app/models/metadata_standard.rb +++ b/app/models/metadata_standard.rb @@ -15,35 +15,34 @@ # rdamsc_id :string # class MetadataStandard < ApplicationRecord - # ============= # = Constants = # ============= # keep "=>" syntax as json_schemer requires string keys SCHEMA_RELATED_ENTITIES = { - "$schema" => "http://json-schema.org/draft-04/schema#", - "type" => "array", - "items" => { - "type" => "object", - "properties" => { - "id" => { "type" => "string" }, - "role" => { "type" => "string" } + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'array', + 'items' => { + 'type' => 'object', + 'properties' => { + 'id' => { 'type' => 'string' }, + 'role' => { 'type' => 'string' } } } - } + }.freeze SCHEMA_LOCATIONS = { - "$schema" => "http://json-schema.org/draft-04/schema#", - "type" => "array", - "items" => { - "type" => "object", - "properties" => { - "url" => { "type" => "string" }, - "type" => { "type" => "string" } + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'array', + 'items' => { + 'type' => 'object', + 'properties' => { + 'url' => { 'type' => 'string' }, + 'type' => { 'type' => 'string' } } } - } + }.freeze # ================ # = Associations = @@ -62,30 +61,29 @@ class MetadataStandard < ApplicationRecord # varchar(255) DEFAULT NULL validates :title, - length: { maximum: 255 } + length: { maximum: 255 } # varchar(255) DEFAULT NULL validates :rdamsc_id, - length: { maximum: 255 } + length: { maximum: 255 } # varchar(255) DEFAULT NULL validates :uri, - length: { maximum: 255 } + length: { maximum: 255 } # json DEFAULT NULL validates :related_entities, - json: { - schema: SCHEMA_RELATED_ENTITIES, - message: ->(errors) { errors } - }, - allow_nil: true + json: { + schema: SCHEMA_RELATED_ENTITIES, + message: ->(errors) { errors } + }, + allow_nil: true # json DEFAULT NULL validates :locations, - json: { - schema: SCHEMA_LOCATIONS, - message: ->(errors) { errors } - }, - allow_nil: true - + json: { + schema: SCHEMA_LOCATIONS, + message: ->(errors) { errors } + }, + allow_nil: true end diff --git a/app/models/repository.rb b/app/models/repository.rb index e4e52a886a..344dcb889b 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -20,74 +20,72 @@ # index_repositories_on_homepage (homepage) # index_repositories_on_uri (uri) # - +# Object that represents a research output repository (e.g. GitHub or Zenodo) class Repository < ApplicationRecord - # ============= # = Constants = # ============= # keep "=>" syntax as json_schemer requires string keys SCHEMA_INFO = { - "$schema" => "http://json-schema.org/draft-04/schema#", - "type" => "object", - "properties" => { - "types" => { - "type" => "array", - "items" => { - "type" => "string" + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'object', + 'properties' => { + 'types' => { + 'type' => 'array', + 'items' => { + 'type' => 'string' } }, - "keywords" => { - "type" => "array", - "items" => { - "type" => "string" + 'keywords' => { + 'type' => 'array', + 'items' => { + 'type' => 'string' } }, - "subjects" => { - "type" => "array", - "items" => { - "type" => "string" + 'subjects' => { + 'type' => 'array', + 'items' => { + 'type' => 'string' } }, - "access" => { - "type" => "string", - "enum" => ["open", "restricted", "closed"] + 'access' => { + 'type' => 'string', + 'enum' => %w[open restricted closed] }, - "provider_types" => { - "type" => "array", - "items" => { - "type" => "string" + 'provider_types' => { + 'type' => 'array', + 'items' => { + 'type' => 'string' } }, - "upload_types" => { - "type" => "array", - "items" => { - "type" => "object", - "properties" => { - "type" => { "type" => "string" }, - "restriction" => { "type" => "string" } + 'upload_types' => { + 'type' => 'array', + 'items' => { + 'type' => 'object', + 'properties' => { + 'type' => { 'type' => 'string' }, + 'restriction' => { 'type' => 'string' } }, - "required" => ["type","restriction"] + 'required' => %w[type restriction] } }, - "policies" => { - "type" => "array", - "items" => { - "type" => "object", - "properties" => { - "name" => { "type" => "string" }, - "url" => { "type" => "string" } + 'policies' => { + 'type' => 'array', + 'items' => { + 'type' => 'object', + 'properties' => { + 'name' => { 'type' => 'string' }, + 'url' => { 'type' => 'string' } }, - "required" => ["name","url"] + 'required' => %w[name url] } }, - "pid_system" => { - "type" => "string" + 'pid_system' => { + 'type' => 'string' } } - } - + }.freeze # ================ # = Associations = @@ -125,32 +123,31 @@ class Repository < ApplicationRecord # varchar(255) NOT NULL validates :name, - presence: { message: PRESENCE_MESSAGE }, - length: { in: 0..255, allow_nil: false } + presence: { message: PRESENCE_MESSAGE }, + length: { in: 0..255, allow_nil: false } # text NOT NULL validates :description, - presence: { message: PRESENCE_MESSAGE } + presence: { message: PRESENCE_MESSAGE } # varchar(255) NOT NULL validates :uri, - presence: { message: PRESENCE_MESSAGE }, - length: { in: 0..255, allow_nil: false } + presence: { message: PRESENCE_MESSAGE }, + length: { in: 0..255, allow_nil: false } # varchar(255) DEFAULT NULL validates :homepage, - length: { maximum: 255 } + length: { maximum: 255 } # varchar(255) DEFAULT NULL validates :contact, - length: { maximum: 255 } + length: { maximum: 255 } # json DEFAULT NULL validates :info, - json: { - schema: SCHEMA_INFO, - message: ->(errors) { errors } - }, - allow_nil: true - + json: { + schema: SCHEMA_INFO, + message: ->(errors) { errors } + }, + allow_nil: true end From 74076b2728cc8ca02a4a514814152802885969f4 Mon Sep 17 00:00:00 2001 From: briri Date: Tue, 5 Apr 2022 08:21:27 -0700 Subject: [PATCH 15/15] updated rspec test for re3data service to use controlled list of terms for repository access types --- spec/services/external_apis/re3data_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/external_apis/re3data_service_spec.rb b/spec/services/external_apis/re3data_service_spec.rb index b56f9d55dc..af1d38f29b 100644 --- a/spec/services/external_apis/re3data_service_spec.rb +++ b/spec/services/external_apis/re3data_service_spec.rb @@ -56,7 +56,7 @@ #{Faker::Lorem.sentence} #{Faker::Internet.url} - #{Faker::Lorem.word} + #{%w[open restricted closed].sample} #{Faker::Lorem.word} #{Faker::Internet.url}