diff --git a/app/controllers/plans_controller.rb b/app/controllers/plans_controller.rb index e6061c984b..53874b74fc 100644 --- a/app/controllers/plans_controller.rb +++ b/app/controllers/plans_controller.rb @@ -209,6 +209,8 @@ def show Template.where(family_id: @plan.template.customization_of).first end + @research_domains = ResearchDomain.all.order(:label) + respond_to :html end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength @@ -260,7 +262,7 @@ def update # TODO: For some reason the `fields_for` isn't adding the # appropriate namespace, so org_id represents our funder funder = org_from_params(params_in: attrs, allow_create: true) - @plan.funder_id = funder.id if funder.present? + @plan.funder_id = funder&.id process_grant(grant_params: plan_params[:grant]) attrs.delete(:grant) attrs = remove_org_selection_params(params_in: attrs) @@ -467,6 +469,8 @@ def plan_params params.require(:plan) .permit(:template_id, :title, :visibility, :description, :identifier, :start_date, :end_date, :org_id, :org_name, :org_crosswalk, + :ethical_issues, :ethical_issues_description, :ethical_issues_report, + :research_domain_id, :funding_status, grant: %i[name value], org: %i[id org_id org_name org_sources org_crosswalk], funder: %i[id org_id org_name org_sources org_crosswalk]) diff --git a/app/javascript/src/plans/editDetails.js b/app/javascript/src/plans/editDetails.js index 2debed7832..f32b817240 100644 --- a/app/javascript/src/plans/editDetails.js +++ b/app/javascript/src/plans/editDetails.js @@ -1,127 +1,154 @@ import { initAutocomplete, scrubOrgSelectionParamsOnSubmit } from '../utils/autoComplete'; import { Tinymce } from '../utils/tinymce.js.erb'; +import toggleConditionalFields from '../utils/conditionalFields'; import getConstant from '../utils/constants'; $(() => { const grantIdField = $('.grant-id-typeahead'); const grantIdHidden = $('input#plan_grant_value'); - Tinymce.init(); - $('#is_test').click((e) => { - $('#plan_visibility').val($(e.target).is(':checked') ? 'is_test' : 'privately_visible'); - }); - - // Toggle the disabled flags - const toggleCheckboxes = (selections) => { - $('#priority-guidance-orgs, #other-guidance-orgs').find('input[type="checkbox"]').each((i, el) => { - const checkbox = $(el); - if (selections.length >= getConstant('MAX_NUMBER_GUIDANCE_SELECTIONS')) { - if (checkbox.is(':checked')) { - checkbox.removeAttr('disabled'); - } else { - checkbox.prop('disabled', true); - } - } else { - checkbox.prop('disabled', false); - } - }); - }; - - // Keep the modal window's guidance selections in line with selections on the main page - const syncGuidance = (ctx) => { - const currentList = $(ctx); - const otherList = (currentList.attr('id') === 'priority-guidance-orgs' ? $('#other-guidance-orgs') : $('#priority-guidance-orgs')); - const selections = currentList.find('input[type="checkbox"]:checked').map((i, el) => $(el).val()).get(); - otherList.find('input[type="checkbox"]').each((i, el) => { - const checkbox = $(el); - // Toggle the checked flag to match the current guidance list - if (selections.indexOf(checkbox.val()) >= 0) { - checkbox.prop('checked', true); - } else { - checkbox.prop('checked', false); - } + const form = $('form.edit_plan'); + + if (form.length > 0) { + Tinymce.init({ selector: '#plan_description' }); + Tinymce.init({ selector: '#plan_ethical_issues_description' }); + + $('#is_test').click((e) => { + $('#plan_visibility').val($(e.target).is(':checked') ? 'is_test' : 'privately_visible'); }); - toggleCheckboxes(selections); - }; - const grantNumberInfo = (grantId) => `Grant number: ${grantId}`; + const ethicalIssues = $('#plan_ethical_issues'); + const funderId = $('#plan_org_id'); + + if (ethicalIssues.length > 0) { + // If the user checks the ethical_issues field then display the other ethics fields + ethicalIssues.on('change', () => { + toggleConditionalFields(ethicalIssues, ethicalIssues.prop('checked')); + }).change(); - const setInitialGrantProjectName = () => { - const grantId = grantIdHidden.val(); - const researchProjects = window.researchProjects; - const researchProject = researchProjects.find((datum) => datum.grant_id === grantId); - if (researchProject) { - grantIdField.val(researchProject.description); + toggleConditionalFields(ethicalIssues, ethicalIssues.prop('checked')); } - }; - - const setUpTypeahead = () => { - if ($('.edit_plan').length) { - // TODO: Convert this over so that it just loads in the controller? - // Follow this pattern: - // if ($('#org-details-org-controls').length > 0) { - // initAutocomplete('#org-details-org-controls .autocomplete'); - // } - - $.get('/research_projects.json', (data) => { - window.researchProjects = data; - const descriptionData = $.map((dataIn, datum) => datum.description); - grantIdField.typeahead({ source: descriptionData }); - }).then(() => { setInitialGrantProjectName(); }); - - grantIdField.on('change', () => { - const current = grantIdField.typeahead('getActive'); - if (current) { - // match or partial match found - const currentResearchProject = window.researchProjects.find((datum) => { - const fixString = (string) => String(string).toLowerCase(); - return fixString(datum.description) === fixString(current); - }); - if (currentResearchProject) { - const grantId = currentResearchProject.grant_id; - $('#grant_number_info').html(grantNumberInfo(grantId)); - if (grantId.length > 0) { - grantIdHidden.val(grantId); - } else { - grantIdHidden.val(grantIdField.val()); - } + if (funderId.length > 0) { + // If the plan has a funder defined then display the other funder fields + funderId.on('change', () => { + toggleConditionalFields(funderId, (funderId.val() !== '{"name":""}' && funderId.val() !== '')); + }).change(); + + toggleConditionalFields(funderId, (funderId.val() !== '{"name":""}' && funderId.val() !== '')); + } + + // Toggle the disabled flags + const toggleCheckboxes = (selections) => { + $('#priority-guidance-orgs, #other-guidance-orgs').find('input[type="checkbox"]').each((i, el) => { + const checkbox = $(el); + if (selections.length >= getConstant('MAX_NUMBER_GUIDANCE_SELECTIONS')) { + if (checkbox.is(':checked')) { + checkbox.removeAttr('disabled'); + } else { + checkbox.prop('disabled', true); } } else { - $('#grant_number_info').html(grantNumberInfo('')); - grantIdHidden.val(grantIdField.val()); + checkbox.prop('disabled', false); } }); - } - }; - - $('#other-guidance-orgs').find('input[type="checkbox"]').click((e) => { - const checkbox = $(e.target); - // Since this is the modal window, copy any selections over to the priority list - if (checkbox.is(':checked')) { - const priorityList = $('#priority-guidance-orgs'); - if (priorityList.find(`input[value="${checkbox.val()}"]`).length <= 0) { - const li = checkbox.closest('li'); - // If its a subgroup copy the whole group otherwise just copy the line - if (li.children('.sublist').length > 0) { - priorityList.append(li.closest('ul').parent().clone()); + }; + + // Keep the modal window's guidance selections in line with selections on the main page + const syncGuidance = (ctx) => { + const currentList = $(ctx); + const otherList = (currentList.attr('id') === 'priority-guidance-orgs' ? $('#other-guidance-orgs') : $('#priority-guidance-orgs')); + const selections = currentList.find('input[type="checkbox"]:checked').map((i, el) => $(el).val()).get(); + otherList.find('input[type="checkbox"]').each((i, el) => { + const checkbox = $(el); + // Toggle the checked flag to match the current guidance list + if (selections.indexOf(checkbox.val()) >= 0) { + checkbox.prop('checked', true); } else { - priorityList.append(li.clone()); + checkbox.prop('checked', false); + } + }); + toggleCheckboxes(selections); + }; + + const grantNumberInfo = (grantId) => `Grant number: ${grantId}`; + + const setInitialGrantProjectName = () => { + const grantId = grantIdHidden.val(); + const researchProjects = window.researchProjects; + const researchProject = researchProjects.find((datum) => datum.grant_id === grantId); + if (researchProject) { + grantIdField.val(researchProject.description); + } + }; + + const setUpTypeahead = () => { + if ($('.edit_plan').length) { + // TODO: Convert this over so that it just loads in the controller? + // Follow this pattern: + // if ($('#org-details-org-controls').length > 0) { + // initAutocomplete('#org-details-org-controls .autocomplete'); + // } + + $.get('/research_projects.json', (data) => { + window.researchProjects = data; + const descriptionData = $.map((dataIn, datum) => datum.description); + grantIdField.typeahead({ source: descriptionData }); + }).then(() => { setInitialGrantProjectName(); }); + + grantIdField.on('change', () => { + const current = grantIdField.typeahead('getActive'); + if (current) { + // match or partial match found + const currentResearchProject = window.researchProjects.find((datum) => { + const fixString = (string) => String(string).toLowerCase(); + return fixString(datum.description) === fixString(current); + }); + if (currentResearchProject) { + const grantId = currentResearchProject.grant_id; + $('#grant_number_info').html(grantNumberInfo(grantId)); + if (grantId.length > 0) { + grantIdHidden.val(grantId); + } else { + grantIdHidden.val(grantIdField.val()); + } + } + } else { + $('#grant_number_info').html(grantNumberInfo('')); + grantIdHidden.val(grantIdField.val()); + } + }); + } + }; + + $('#other-guidance-orgs').find('input[type="checkbox"]').click((e) => { + const checkbox = $(e.target); + // Since this is the modal window, copy any selections over to the priority list + if (checkbox.is(':checked')) { + const priorityList = $('#priority-guidance-orgs'); + if (priorityList.find(`input[value="${checkbox.val()}"]`).length <= 0) { + const li = checkbox.closest('li'); + // If its a subgroup copy the whole group otherwise just copy the line + if (li.children('.sublist').length > 0) { + priorityList.append(li.closest('ul').parent().clone()); + } else { + priorityList.append(li.clone()); + } } } - } - syncGuidance(checkbox.closest('ul[id]')); - }); + syncGuidance(checkbox.closest('ul[id]')); + }); - $('#priority-guidance-orgs').find('input[type="checkbox"]').click((e) => { - syncGuidance($(e.target).closest('ul[id]')); - }); + $('#priority-guidance-orgs').find('input[type="checkbox"]').click((e) => { + syncGuidance($(e.target).closest('ul[id]')); + }); - initAutocomplete('#funder-org-controls .autocomplete'); - // Scrub out the large arrays of data used for the Org Selector JS so that they - // are not a part of the form submissiomn - scrubOrgSelectionParamsOnSubmit('form.edit_plan'); + initAutocomplete('#funder-org-controls .autocomplete'); + // Scrub out the large arrays of data used for the Org Selector JS so that they + // are not a part of the form submissiomn + scrubOrgSelectionParamsOnSubmit('form.edit_plan'); - toggleCheckboxes($('#priority-guidance-orgs input[type="checkbox"]:checked').map((i, el) => $(el).val()).get()); + toggleCheckboxes($('#priority-guidance-orgs input[type="checkbox"]:checked').map((i, el) => $(el).val()).get()); - setUpTypeahead(); + setUpTypeahead(); + } }); diff --git a/app/javascript/src/utils/conditionalFields.js b/app/javascript/src/utils/conditionalFields.js new file mode 100644 index 0000000000..df4a2af42f --- /dev/null +++ b/app/javascript/src/utils/conditionalFields.js @@ -0,0 +1,43 @@ +// Logic to hide/dsiplay a set of fields based on whether or not a related checkbox is clicked +// +// Expecting the checkbox and the corresponding fields to be wrapped in +// a element +// +// For example see: app/views/plans/_edit_details.html.erb +// app/javascript/src/plans/editDetails.js +// +import { Tinymce } from './tinymce.js.erb'; + +// Expecting `context` to be the field that triggers the hide/show of the corresponding fields +export default function toggleConditionalFields(context, showThem) { + const container = $(context).closest('conditional'); + + if (container.length > 0) { + if (showThem === true) { + container.find('.toggleable-field').show(); + + // Resize any TinyMCE editors + container.find('.toggleable-field').find('.tinymce').each((_idx, el) => { + const tinymceEditor = Tinymce.findEditorById($(el).attr('id')); + if (tinymceEditor) { + $(tinymceEditor.iframeElement).height(tinymceEditor.settings.autoresize_min_height); + } + }); + } else { + // Clear the contents of any textarea select boxes or input fields + container.find('.toggleable-field').find('input, textarea, select').val('').change(); + + // TODO: clear check boxes and radio buttons as needed + + // Clear the contents of any TinyMCE editors + container.find('.toggleable-field').find('.tinymce').each((_idx, el) => { + const tinymceEditor = Tinymce.findEditorById($(el).attr('id')); + if (tinymceEditor) { + tinymceEditor.setContent(''); + } + }); + + container.find('.toggleable-field').hide(); + } + } +} diff --git a/app/models/concerns/identifiable.rb b/app/models/concerns/identifiable.rb index 3593d8a061..3aea3fc227 100644 --- a/app/models/concerns/identifiable.rb +++ b/app/models/concerns/identifiable.rb @@ -53,7 +53,7 @@ def self.from_identifiers(array:) # gets the identifier for the scheme def identifier_for_scheme(scheme:) scheme = IdentifierScheme.by_name(scheme.downcase).first if scheme.is_a?(String) - identifiers.select { |id| id.identifier_scheme == scheme }.first + identifiers.select { |id| id.identifier_scheme == scheme }.last end # Combines the existing identifiers with the new ones diff --git a/app/models/plan.rb b/app/models/plan.rb index 8ac2ed1a32..a5fe102007 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -9,18 +9,9 @@ # # id :integer not null, primary key # complete :boolean default(FALSE) -# data_contact :string -# data_contact_email :string -# data_contact_phone :string # description :text # feedback_requested :boolean default(FALSE) -# funder_name :string -# grant_number :string # identifier :string -# principal_investigator :string -# principal_investigator_email :string -# principal_investigator_identifier :string -# principal_investigator_phone :string # title :string # visibility :integer default(3), not null # created_at :datetime @@ -29,7 +20,11 @@ # org_id :integer # funder_id :integer # grant_id :integer -# api_client_id :integer +# research_domain_id :bigint +# funding_status :integer +# ethical_issues :boolean +# ethical_issues_description :text +# ethical_issues_report :string # # Indexes # @@ -42,10 +37,9 @@ # # fk_rails_... (template_id => templates.id) # fk_rails_... (org_id => orgs.id) +# fk_rails_... (research_domain_id => research_domains.id) # -# TODO: Drop the funder_name and grant_number columns once the funder_id has -# been back filled and we're removing the is_other org stuff class Plan < ApplicationRecord include ConditionalUserMailer @@ -74,6 +68,8 @@ class Plan < ApplicationRecord enum visibility: %i[organisationally_visible publicly_visible is_test privately_visible] + enum funding_status: %i[planned funded denied] + alias_attribute :name, :title attribute :visibility, :integer, default: 3 @@ -88,6 +84,8 @@ class Plan < ApplicationRecord belongs_to :funder, class_name: "Org", optional: true + belongs_to :research_domain, optional: true + has_many :phases, through: :template has_many :sections, through: :phases @@ -117,10 +115,6 @@ class Plan < ApplicationRecord has_many :contributors, dependent: :destroy - has_one :grant, as: :identifiable, dependent: :destroy, class_name: "Identifier" - - belongs_to :api_client, optional: true - # ===================== # = Nested Attributes = # ===================== @@ -172,10 +166,6 @@ class Plan < ApplicationRecord ) } - # TODO: Add in a left join here so we can search contributors as well when - # we move to Rails 5: - # OR lower(contributors.name) LIKE lower(:search_pattern) - # OR lower(identifiers.value) LIKE lower(:search_pattern)", scope :search, lambda { |term| if date_range?(term: term) joins(:template, roles: [user: :org]) @@ -184,13 +174,14 @@ class Plan < ApplicationRecord else search_pattern = "%#{term}%" joins(:template, roles: [user: :org]) + .left_outer_joins(:identifiers, :contributors) .where(Role.creator_condition) .where("lower(plans.title) LIKE lower(:search_pattern) OR lower(orgs.name) LIKE lower (:search_pattern) OR lower(orgs.abbreviation) LIKE lower (:search_pattern) OR lower(templates.title) LIKE lower(:search_pattern) - OR lower(plans.principal_investigator) LIKE lower(:search_pattern) - OR lower(plans.principal_investigator_identifier) LIKE lower(:search_pattern)", + OR lower(contributors.name) LIKE lower(:search_pattern) + OR lower(identifiers.value) LIKE lower(:search_pattern)", search_pattern: search_pattern) end } @@ -568,6 +559,29 @@ def landing_page identifiers.select { |i| %w[doi ark].include?(i.identifier_format) }.first end + # Since the Grant is not a normal AR association, override the getter and setter + def grant + Identifier.find_by(id: grant_id) + end + + # Helper method to convert the grant id value entered by the user into an Identifier + # works with both controller params or an instance of Identifier + def grant=(params) + val = params.present? ? params[:value] : nil + current = grant + + # Remove it if it was blanked out by the user + current.destroy if current.present? && !val.present? + return unless val.present? + + # Create the Identifier if it doesn't exist and then set the id + current.update(value: val) if current.present? && current.value != val + return if current.present? + + current = Identifier.create(identifiable: self, value: val) + self.grant_id = current.id + end + private # Validation to prevent end date from coming before the start date diff --git a/app/models/research_domain.rb b/app/models/research_domain.rb new file mode 100644 index 0000000000..be049807c9 --- /dev/null +++ b/app/models/research_domain.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: research_domains +# +# id :bigint not null, primary key +# identifier :string not null +# label :string not null +# created_at :datetime not null +# updated_at :datetime not null +# parent_id :bigint +# +# Indexes +# +# index_research_domains_on_parent_id (parent_id) +# +# Foreign Keys +# +# fk_rails_... (parent_id => research_domains.id) +# +class ResearchDomain < ApplicationRecord + + # ================ + # = Associations = + # ================ + + # Self join + has_many :sub_fields, class_name: "ResearchDomain", foreign_key: "parent_id" + belongs_to :parent, class_name: "ResearchDomain", optional: true + +end diff --git a/app/models/research_output.rb b/app/models/research_output.rb index a180615207..858ef06d10 100644 --- a/app/models/research_output.rb +++ b/app/models/research_output.rb @@ -8,13 +8,9 @@ # abbreviation :string # access :integer default(0), not null # byte_size :bigint -# coverage_end :datetime -# coverage_region :string -# coverage_start :datetime # description :text # display_order :integer # is_default :boolean default("false") -# mandatory_attribution :text # output_type :integer default(3), not null # output_type_description :string # personal_data :boolean @@ -47,7 +43,6 @@ class ResearchOutput < ApplicationRecord # ================ belongs_to :plan, optional: true - belongs_to :mime_type, optional: true # =============== # = Validations = @@ -58,24 +53,11 @@ class ResearchOutput < ApplicationRecord # Ensure presence of the :output_type_description if the user selected 'other' validates_presence_of :output_type_description, if: -> { other? }, message: PRESENCE_MESSAGE - # Ensure that :coverage_start comes before :coverage_end - validate :end_date_after_start_date # ==================== # = Instance methods = # ==================== - # :mime_type is only applicable for certain :output_types - # This method returns the applicable :mime_types - def available_mime_types - cat = %w[audio video] if audiovisual? || sound? - cat = %w[image] if image? - cat = %w[model] if model_representation? - cat = %w[text] if data_paper? || dataset? || text? - - cat.present? ? MimeType.where(category: cat).order(:description) : [] - end - # TODO: placeholders for once the License, Repository, Metadata Standard and # Resource Type Lookups feature is built. # @@ -109,14 +91,4 @@ def resource_types [] end - private - - # Validation to prevent end date from coming before the start date - def end_date_after_start_date - # allow nil values - return true if coverage_end.blank? || coverage_start.blank? - - errors.add(:coverage_end, _("must be after the start date")) if coverage_end < coverage_start - end - end diff --git a/app/presenters/api/v1/funding_presenter.rb b/app/presenters/api/v1/funding_presenter.rb index ccd1fa0157..2216ace28c 100644 --- a/app/presenters/api/v1/funding_presenter.rb +++ b/app/presenters/api/v1/funding_presenter.rb @@ -11,9 +11,16 @@ 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? && plan.grant_number.present? - - "granted" + return "planned" unless plan.present? + + case plan.funding_status + when "funded" + "granted" + when "denied" + "rejected" + else + "planned" + end end end diff --git a/app/presenters/api/v1/plan_presenter.rb b/app/presenters/api/v1/plan_presenter.rb index 406a6caf66..a8e2510390 100644 --- a/app/presenters/api/v1/plan_presenter.rb +++ b/app/presenters/api/v1/plan_presenter.rb @@ -16,6 +16,8 @@ def initialize(plan:) @plan = plan + @data_contact = @plan.owner + # Attach the first data_curation role as the data_contact, otherwise # add the contributor to the contributors array @plan.contributors.each do |contributor| diff --git a/app/services/api/v1/contextual_error_service.rb b/app/services/api/v1/contextual_error_service.rb index a6a94494b9..7dc67ad464 100644 --- a/app/services/api/v1/contextual_error_service.rb +++ b/app/services/api/v1/contextual_error_service.rb @@ -54,7 +54,7 @@ def contextualize(errors:, context: "DMP") def valid_plan?(plan:) plan.valid? && (plan.funder.blank? || plan.funder.valid?) && - (plan.grant.blank? || plan.grant.valid?) + (plan.grant.blank? || plan.grant.value.present?) end end diff --git a/app/services/api/v1/deserialization/plan.rb b/app/services/api/v1/deserialization/plan.rb index 5a4492f47b..5fbd2101bc 100644 --- a/app/services/api/v1/deserialization/plan.rb +++ b/app/services/api/v1/deserialization/plan.rb @@ -58,6 +58,12 @@ def deserialize(json: {}) return nil unless plan.present? && plan.template.present? plan.description = json[:description] if json[:description].present? + issues = Api::V1::ConversionService.yes_no_unknown_to_boolean( + json[:ethical_issues_exist] + ) + plan.ethical_issues = issues + plan.ethical_issues_description = json[:ethical_issues_description] + plan.ethical_issues_report = json[:ethical_issues_report] # TODO: Handle ethical issues when the Question is in place diff --git a/app/services/api/v1/deserialization_service.rb b/app/services/api/v1/deserialization_service.rb index 73128217cf..2291781a27 100644 --- a/app/services/api/v1/deserialization_service.rb +++ b/app/services/api/v1/deserialization_service.rb @@ -52,7 +52,7 @@ def translate_role(role:) role = role.gsub("#{url}/", "").downcase if role.include?(url) # Return the role if its a valid one otherwise defualt - return role if ::Contributor.new.respond_to?(role.downcase.to_sym) + return role if ::Contributor.new.all_roles.include?(role.downcase.to_sym) default end diff --git a/app/views/api/v1/datasets/_show.json.jbuilder b/app/views/api/v1/datasets/_show.json.jbuilder index e70fe44546..3a3bb0b3b3 100644 --- a/app/views/api/v1/datasets/_show.json.jbuilder +++ b/app/views/api/v1/datasets/_show.json.jbuilder @@ -20,3 +20,10 @@ json.distribution [plan] do |distribution| json.array! ["application/pdf"] 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}"] + end +end diff --git a/app/views/api/v1/plans/_funding.json.jbuilder b/app/views/api/v1/plans/_funding.json.jbuilder index bd3a6cc280..725cda1358 100644 --- a/app/views/api/v1/plans/_funding.json.jbuilder +++ b/app/views/api/v1/plans/_funding.json.jbuilder @@ -19,4 +19,23 @@ if plan.grant_id.present? && plan.grant.present? json.partial! "api/v1/identifiers/show", identifier: plan.grant end end -json.funding_status plan.grant.present? ? "granted" : "planned" + +json.funding_status Api::V1::FundingPresenter.status(plan: plan) + +# DMPTool extensions to the RDA common metadata standard +# ------------------------------------------------------ + +# We collect a user entered ID on the form, so this is a way to convey it to other systems +# The ID would typically be something relevant to the funder or research organization +if plan.identifier.present? + json.dmproadmap_funding_opportunity_id do + json.partial! "api/v1/identifiers/show", identifier: Identifier.new(identifiable: plan, + value: plan.identifier) + end +end + +# Since the Plan owner (aka contact) and contributor orgs could be different than the +# one associated with the Plan, we add it here. +json.dmproadmap_funded_affiliations [plan.org] do |funded_org| + json.partial! "api/v1/orgs/show", org: funded_org +end diff --git a/app/views/api/v1/plans/_show.json.jbuilder b/app/views/api/v1/plans/_show.json.jbuilder index d0311aeb0c..6f8d226f5f 100644 --- a/app/views/api/v1/plans/_show.json.jbuilder +++ b/app/views/api/v1/plans/_show.json.jbuilder @@ -15,10 +15,9 @@ json.language Api::V1::LanguagePresenter.three_char_code( json.created plan.created_at.to_formatted_s(:iso8601) json.modified plan.updated_at.to_formatted_s(:iso8601) -# TODO: Update this to pull from the appropriate question once the work is complete -json.ethical_issues_exist "unknown" -# json.ethical_issues_description "" -# json.ethical_issues_report "" +json.ethical_issues_exist Api::V1::ConversionService.boolean_to_yes_no_unknown(plan.ethical_issues) +json.ethical_issues_description plan.ethical_issues_description +json.ethical_issues_report plan.ethical_issues_report id = presenter.identifier if id.present? diff --git a/app/views/plans/_edit_details.html.erb b/app/views/plans/_edit_details.html.erb index c2bbcc43dd..e6cc4a747a 100644 --- a/app/views/plans/_edit_details.html.erb +++ b/app/views/plans/_edit_details.html.erb @@ -5,7 +5,8 @@
<%= render partial: "plans/project_details", - locals: { form: f, plan: plan, orgs: orgs, org_partial: org_partial } %> + locals: { form: f, plan: plan, orgs: orgs, org_partial: org_partial, + research_domains: @research_domains } %> <%= f.button(_('Save'), class: "btn btn-default", type: "submit") %>
diff --git a/app/views/plans/_project_details.html.erb b/app/views/plans/_project_details.html.erb index 3e1a9c0ca7..eba8192601 100644 --- a/app/views/plans/_project_details.html.erb +++ b/app/views/plans/_project_details.html.erb @@ -1,8 +1,13 @@ -<%# locals: form, plan %> +<%# locals: form, plan, research_domains %> -<% project_title_tooltip = _('If applying for funding, state the name exactly as in the grant proposal.') %> -<% project_abstract_tooltip = _("Briefly summarise your research project to help others understand the purposes for which the data are being collected or created.") %> -<% id_tooltip = _('A pertinent ID as determined by the funder and/or organisation.') %> +<% +project_title_tooltip = _('If applying for funding, state the name exactly as in the grant proposal.') +project_abstract_tooltip = _("Briefly summarise your research project to help others understand the purposes for which the data are being collected or created.") +id_tooltip = _('A pertinent ID as determined by the funder and/or organisation.') +ethics_tooltip = _("Whether there are any potential ethical issues related to data that this DMP describes") +ethics_description_tooltip = _("Description of the ethical issues") +ethics_report_tooltip = _("Link to a protocol from a meeting with an ethics commitee") +%>
@@ -36,6 +41,22 @@
+<% if Rails.configuration.x.madmp.enable_research_domain %> +
+
+ <%= form.label(:research_domain_id, _("Research domain"), class: "control-label") %> + + <% options = research_domains.map { |rd| [rd.label, rd.id] } %> + <%= form.select :research_domain_id, options_for_select(options, form.object.research_domain_id), + { + include_blank: _("- Please select one -"), + selected: form.object.research_domain_id + }, + { class: "form-control" } %> +
+
+<% end %> +
<%= form.label(:start_date, _("Project Start"), class: "control-label") %> @@ -81,18 +102,44 @@
<% end %> -
- <% if plan.template.org == plan.funder %> - <%# If the plan's funder is the same as the template owner then just - display the identifiers %> -
- <%= form.label(:funder_name, _('Funder'), class: 'control-label') %> +<% if Rails.configuration.x.madmp.enable_ethical_issues %> + +
+
+
+ <%= form.label(:ethical_issues, class: 'control-label', title: ethics_tooltip) do %> + <%= form.check_box(:ethical_issues) %> + <%= _('Research outputs may have ethical concerns') %> + <% end %> +
+
-
- <%= plan.funder.name %>: +
+
+ <%= form.label(:ethical_issues_description, _('Describe any ethical concerns'), class: 'control-label') %> +
+
+ <%= ethics_description_tooltip %> + <%= form.text_area :ethical_issues_description, rows: 6, class: 'form-control tinymce', + "aria-required": false %> +
- <% else %> - <%# Otherwise display the Org typeahead for funders %> +
+
+ <%= form.label(:ethical_issues_report, _('Ethical protocols'), class: 'control-label') %> +
+
+ <%= ethics_report_tooltip %> + <%= form.url_field(:ethical_issues_report, class: "form-control", "aria-required": false, + 'data-toggle': 'tooltip', + title: ethics_report_tooltip) %> +
+
+ +<% end %> + + +
<%= fields_for :funder, plan.funder do |funder_fields| %> <%= render partial: org_partial, @@ -106,28 +153,41 @@ } %> <% end %>
- <% end %> -
+
-
- <%= form.fields_for :grant, @plan.grant do |grant_fields| %> +
- <%= grant_fields.label(:value, _("Grant number/url"), class: "control-label") %> + <%= form.label(:funding_status, _("Funding status"), class: "control-label") %>
-
- <%# If the OpenAire grant typeahead if enabled use it %> - <% if Rails.configuration.x.open_aire.active %> - - <%= grant_fields.text_field :name, value: @plan.grant&.value, class: "form-control grant-id-typeahead", - autocomplete: "off", aria: { required: false } %> - <%= grant_fields.hidden_field :value %> - Grant number: <%= @plan.grant_number %> - <% else %> - <%= grant_fields.text_field(:value, class: "form-control", - data: { toggle: "tooltip" }, - title: _("Provide a URL to the award's landing page if possible, if not please provide the award/grant number.")) %> - <% end %> + <% funding_statuses = Plan.funding_statuses.map { |status| [status[0].capitalize, status[0]] } %> + <%= form.select :funding_status, options_for_select(funding_statuses, form.object.funding_status), + { + include_blank: _("- Please select one -"), + selected: form.object.funding_status + }, + { class: "form-control" } %>
- <% end %> -
+ + <%= form.fields_for :grant, plan.grant do |grant_fields| %> +
+ <%= grant_fields.label(:value, _("Grant number/url"), class: "control-label") %> +
+ +
+ <%# If the OpenAire grant typeahead if enabled use it %> + <% if Rails.configuration.x.open_aire.active %> + + <%= grant_fields.text_field :name, value: plan.grant&.value, class: "form-control grant-id-typeahead", + autocomplete: "off", aria: { required: false } %> + <%= grant_fields.hidden_field :value %> + Grant number: <%= plan.grant&.value %> + <% else %> + <%= grant_fields.text_field(:value, class: "form-control", + data: { toggle: "tooltip" }, + title: _("Provide a URL to the award's landing page if possible, if not please provide the award/grant number.")) %> + <% end %> +
+ <% end %> +
+
diff --git a/app/views/shared/export/_plan_coversheet.erb b/app/views/shared/export/_plan_coversheet.erb index 3a50c75dcd..df16331646 100644 --- a/app/views/shared/export/_plan_coversheet.erb +++ b/app/views/shared/export/_plan_coversheet.erb @@ -41,8 +41,8 @@

<%= _("Last modified: ") %><%= l(@plan.updated_at.to_date, formats: :short) %>


- <% if @plan.grant_number.present? %> -

<%= _("Grant number / URL: ") %><%= @plan.grant_number %>


+ <% if @plan.grant.present? %> +

<%= _("Grant number / URL: ") %><%= @plan.grant.value %>


<% end %> <% if @public_plan %> diff --git a/config/initializers/_dmproadmap.rb b/config/initializers/_dmproadmap.rb index 1b1fff6ed9..0d44e72c5f 100644 --- a/config/initializers/_dmproadmap.rb +++ b/config/initializers/_dmproadmap.rb @@ -204,6 +204,12 @@ class Application < Rails::Application # ------------------------------------------------------------------------ # config.x.recaptcha.enabled = false + # --------------------------------------------------- # + # Machine Actionable / Networked DMP Features (maDMP) # + # --------------------------------------------------- # + config.x.madmp.enable_ethical_issues = false + config.x.madmp.enable_research_domain = false + end end diff --git a/db/migrate/20210729204238_create_research_domains.rb b/db/migrate/20210729204238_create_research_domains.rb new file mode 100644 index 0000000000..3cb6414b0c --- /dev/null +++ b/db/migrate/20210729204238_create_research_domains.rb @@ -0,0 +1,12 @@ +class CreateFos < ActiveRecord::Migration[5.2] + def change + create_table :research_domains do |t| + t.string :identifier, null: false + t.string :label, null: false + t.references :parent, foreign_key: { to_table: :research_domains } + t.timestamps + end + + add_reference :plans, :research_domain, index: true + end +end diff --git a/db/migrate/20210729204412_add_ethical_issues_and_funding_status_to_plans.rb b/db/migrate/20210729204412_add_ethical_issues_and_funding_status_to_plans.rb new file mode 100644 index 0000000000..a1432822e0 --- /dev/null +++ b/db/migrate/20210729204412_add_ethical_issues_and_funding_status_to_plans.rb @@ -0,0 +1,9 @@ +class AddEthicalIssuesAndFundingStatusToPlans < ActiveRecord::Migration[5.2] + def change + add_column :plans, :ethical_issues, :boolean + add_column :plans, :ethical_issues_description, :text + add_column :plans, :ethical_issues_report, :string + + add_column :plans, :funding_status, :integer, null: true + end +end diff --git a/db/migrate/20210729204611_madmp_cleanup.rb b/db/migrate/20210729204611_madmp_cleanup.rb new file mode 100644 index 0000000000..bfea82c298 --- /dev/null +++ b/db/migrate/20210729204611_madmp_cleanup.rb @@ -0,0 +1,34 @@ +class MadmpCleanup < ActiveRecord::Migration[5.2] + def change + + # Decided not to ask users for the mime type when defining research outputs + drop_table :mime_types + remove_column :research_outputs, :mime_type_id + + # Remove attributes found in the RDA common standard that we decided not to use + remove_column :research_outputs, :mandatory_attribution + remove_column :research_outputs, :coverage_region + 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 + remove_column :plans, :data_contact_email + remove_column :plans, :data_contact_phone + + remove_column :plans, :principal_investigator + remove_column :plans, :principal_investigator_email + remove_column :plans, :principal_investigator_identifier + remove_column :plans, :principal_investigator_phone + + + # Remove the old funder and grant fields since they have been replaced by associations + remove_column :plans, :funder_name + remove_column :plans, :grant_number + end +end diff --git a/db/schema.rb b/db/schema.rb index afeebf74be..e36701b4ce 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: 2020_12_16_140226) do +ActiveRecord::Schema.define(version: 2021_07_29_204611) do create_table "annotations", id: :integer, force: :cascade do |t| t.integer "question_id" @@ -157,15 +157,6 @@ t.boolean "default_language" end - create_table "mime_types", force: :cascade do |t| - t.string "description", null: false - t.string "category", null: false - t.string "value", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["value"], name: "index_mime_types_on_value" - end - create_table "notes", id: :integer, force: :cascade do |t| t.integer "user_id" t.text "text" @@ -257,18 +248,9 @@ t.integer "template_id" t.datetime "created_at" t.datetime "updated_at" - t.string "grant_number" t.string "identifier" t.text "description" - t.string "principal_investigator" - t.string "principal_investigator_identifier" - t.string "data_contact" - t.string "funder_name" t.integer "visibility", default: 3, null: false - t.string "data_contact_email" - t.string "data_contact_phone" - t.string "principal_investigator_email" - t.string "principal_investigator_phone" t.boolean "feedback_requested", default: false t.boolean "complete", default: false t.integer "org_id" @@ -276,11 +258,16 @@ t.integer "grant_id" t.datetime "start_date" t.datetime "end_date" - t.integer "api_client_id" + t.boolean "ethical_issues" + t.text "ethical_issues_description" + t.string "ethical_issues_report" + t.integer "funding_status" + t.bigint "research_domain_id" 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 ["template_id"], name: "index_plans_on_template_id" + t.index ["research_domain_id"], name: "index_plans_on_research_domain_id" end create_table "plans_guidance_groups", id: :integer, force: :cascade do |t| @@ -355,6 +342,15 @@ t.integer "super_region_id" end + create_table "research_domains", force: :cascade do |t| + t.string "identifier", null: false + t.string "label", null: false + t.bigint "parent_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["parent_id"], name: "index_research_domains_on_parent_id" + end + create_table "research_outputs", force: :cascade do |t| t.integer "plan_id" t.integer "output_type", default: 3, null: false @@ -364,16 +360,11 @@ t.integer "display_order" t.boolean "is_default" t.text "description" - t.integer "mime_type_id" t.integer "access", default: 0, null: false t.datetime "release_date" t.boolean "personal_data" t.boolean "sensitive_data" t.bigint "byte_size" - t.text "mandatory_attribution" - t.datetime "coverage_start" - t.datetime "coverage_end" - t.string "coverage_region" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["output_type"], name: "index_research_outputs_on_output_type" diff --git a/lib/tasks/utils/external_api.rake b/lib/tasks/utils/external_api.rake index f74986d977..7b363831f5 100644 --- a/lib/tasks/utils/external_api.rake +++ b/lib/tasks/utils/external_api.rake @@ -1,60 +1,101 @@ # frozen_string_literal: true -require "httparty" - -# rubocop:disable Metrics/BlockLength +# rubocop:disable Metrics/BlockLength, Layout/LineLength namespace :external_api do - # MIME Types - # ------------------------------------- - desc "Fetch all of the latest MIME types and load into the DB" - task load_mime_types: :environment do - %w[application.csv audio.csv font.csv image.csv message.csv model.csv - multipart.csv text.csv video.csv].each do |file_name| - url = "https://www.iana.org/assignments/media-types" - fetch_and_process_mime_type_file(url: "#{url}/#{file_name}") - end - end - - private - - # MIME Types - # ------------------------------------- - def fetch_and_process_mime_type_file(url:) - p "Processing #{url}" - body = fetch_mime_type_csv_file(url: url) - p " Unable to process the specified URL" unless body.present? - - csv = CSV.parse(body, headers: true, force_quotes: true, encoding: "iso-8859-1:utf-8") - # rubocop:disable Layout/LineLength - p " Invalid CSV format. Expecting a 'Name' and 'Template' column" unless csv.headers.include?("Name") && - csv.headers.include?("Template") - # rubocop:enable Layout/LineLength - process_mime_type_file(csv: csv) - p " Done" - rescue StandardError => e - p " Error processing CSV content - #{e.message}" - 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 + # to fetch the list from there instead of the hard-coded list below which was derived from: + # https://www.oecd.org/science/inno/38235147.pdf + [ + { + identifier: "1", + label: "Natural sciences", + children: [ + { identifier: "1.1", label: "Mathematics" }, + { identifier: "1.2", label: "Computer and information sciences" }, + { identifier: "1.3", label: "Physical sciences" }, + { identifier: "1.4", label: "Chemical sciences" }, + { identifier: "1.5", label: "Earth and related environmental sciences" }, + { identifier: "1.6", label: "Biological sciences" }, + { identifier: "1.7", label: "Other natural sciences" } + ] + }, + { + identifier: "2", + label: "Engineering and Technology", + children: [ + { identifier: "2.1", label: "Civil engineering" }, + { identifier: "2.2", label: "Electrical engineering, electronic engineering, information engineering" }, + { identifier: "2.3", label: "Mechanical engineering" }, + { identifier: "2.4", label: "Chemical engineering" }, + { identifier: "2.5", label: "Materials engineering" }, + { identifier: "2.6", label: "Medical engineering" }, + { identifier: "2.7", label: "Environmental engineering" }, + { identifier: "2.8", label: "Environmental biotechnology" }, + { identifier: "2.9", label: "Industrial Biotechnology" }, + { identifier: "2.10", label: "Nano-technology" }, + { identifier: "2.11", label: "Other engineering and technologies" } + ] + }, + { + identifier: "3", + label: "Medical and Health Sciences", + children: [ + { identifier: "3.1", label: "Basic medicine" }, + { identifier: "3.2", label: "Clinical medicine" }, + { identifier: "3.3", label: "Health sciences" }, + { identifier: "3.4", label: "Health biotechnology" }, + { identifier: "3.5", label: "Other medical sciences" } + ] + }, + { + identifier: "4", + label: "Agricultural Sciences", + children: [ + { identifier: "4.1", label: "Agriculture, forestry, and fisheries" }, + { identifier: "4.2", label: "Animal and dairy science" }, + { identifier: "4.3", label: "Veterinary science" }, + { identifier: "4.4", label: "Agricultural biotechnology" }, + { identifier: "4.5", label: "Other agricultural sciences" } + ] + }, + { + identifier: "5", + label: "Social Sciences", + children: [ + { identifier: "5.1", label: "Psychology" }, + { identifier: "5.2", label: "Economics and business" }, + { identifier: "5.3", label: "Educational sciences" }, + { identifier: "5.4", label: "Sociology" }, + { identifier: "5.5", label: "Law" }, + { identifier: "5.6", label: "Political science" }, + { identifier: "5.7", label: "Social and economic geography" }, + { identifier: "5.8", label: "Media and communications" }, + { identifier: "5.7", label: "Other social sciences" } + ] + }, + { + identifier: "6", + label: "Humanities", + children: [ + { identifier: "6.1", label: "History and archaeology" }, + { identifier: "6.2", label: "Languages and literature" }, + { identifier: "6.3", label: "Philosophy, ethics and religion" }, + { identifier: "6.4", label: "Art (arts, history of arts, performing arts, music)" }, + { identifier: "6.5", label: "Other humanities" } + ] + } + ].each do |fos| + p "#{fos[:identifier]} - #{fos[:label]}" + parent = ResearchDomain.find_or_create_by(identifier: fos[:identifier], label: fos[:label]) - def process_mime_type_file(csv:) - return unless csv.is_a?(CSV::Table) - - csv.each do |line| - next unless line["Template"].present? && line["Name"].present? - - type = MimeType.find_or_initialize_by(value: line["Template"].downcase) - type.description = line["Name"] - type.category = line["Template"].split("/").first.downcase - type.save + fos[:children].each do |child| + child[:parent_id] = parent.id + p " #{child[:identifier]} - #{child[:label]}" + ResearchDomain.find_or_create_by(child) + end end end - - def fetch_mime_type_csv_file(url:) - return nil unless url.present? - - payload = HTTParty.get(url, debug: false) - return nil unless payload.code == 200 - - payload.body - end end -# rubocop:enable Metrics/BlockLength +# rubocop:enable Metrics/BlockLength, Layout/LineLength diff --git a/spec/factories/plans.rb b/spec/factories/plans.rb index ebd56acf16..20bfbdd9a3 100644 --- a/spec/factories/plans.rb +++ b/spec/factories/plans.rb @@ -6,18 +6,13 @@ # # id :integer not null, primary key # complete :boolean default(FALSE) -# data_contact :string -# data_contact_email :string -# data_contact_phone :string # description :text +# ethical_issues :boolean +# ethical_issues_description :text +# ethical_issues_report :string # feedback_requested :boolean default(FALSE) -# funder_name :string -# grant_number :string +# funding_status :integer # identifier :string -# principal_investigator :string -# principal_investigator_email :string -# principal_investigator_identifier :string -# principal_investigator_phone :string # title :string # visibility :integer default(3), not null # created_at :datetime @@ -26,7 +21,7 @@ # org_id :integer # funder_id :integer # grant_id :integer -# api_client_id :integer +# research_domain_id :bigint # # Indexes # @@ -39,6 +34,7 @@ # # fk_rails_... (template_id => templates.id) # fk_rails_... (org_id => orgs.id) +# fk_rails_... (research_domain_id => research_domains.id) # FactoryBot.define do @@ -46,21 +42,16 @@ title { Faker::Company.bs } template org - # TODO: Drop this column once the funder_id has been back filled - # and we're removing the is_other org stuff - grant_number { SecureRandom.rand(1_000) } identifier { SecureRandom.hex } description { Faker::Lorem.paragraph } - principal_investigator { Faker::Name.name } - # TODO: Drop this column once the funder_id has been back filled - # and we're removing the is_other org stuff - funder_name { Faker::Company.name } - data_contact_email { Faker::Internet.safe_email } - principal_investigator_email { Faker::Internet.safe_email } feedback_requested { false } complete { false } start_date { Time.now } end_date { start_date + 2.years } + ethical_issues { [true, false].sample } + ethical_issues_description { Faker::Lorem.paragraph } + ethical_issues_report { Faker::Internet.url } + funding_status { Plan.funding_statuses.keys.sample } transient do phases { 0 } diff --git a/spec/factories/research_domains.rb b/spec/factories/research_domains.rb new file mode 100644 index 0000000000..7d4017f924 --- /dev/null +++ b/spec/factories/research_domains.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: research_domains +# +# id :bigint(8) not null, primary key +# identifier :string(255) not null +# label :string(255) not null +# created_at :datetime not null +# updated_at :datetime not null +# parent_id :bigint(8) +# +# Indexes +# +# index_research_domain_on_parent_id (parent_id) +# +# Foreign Keys +# +# fk_rails_... (parent_id => research_domains.id) +# +FactoryBot.define do + factory :research_domain do + identifier { SecureRandom.uuid } + label { Faker::Lorem.unique.word } + uri { Faker::Internet.url } + end +end diff --git a/spec/factories/research_outputs.rb b/spec/factories/research_outputs.rb index ccc7204d83..807ecd8ad4 100644 --- a/spec/factories/research_outputs.rb +++ b/spec/factories/research_outputs.rb @@ -8,13 +8,9 @@ # abbreviation :string # access :integer default(0), not null # byte_size :bigint -# coverage_end :datetime -# coverage_region :string -# coverage_start :datetime # description :text # display_order :integer # is_default :boolean default("false") -# mandatory_attribution :text # output_type :integer default(3), not null # output_type_description :string # personal_data :boolean @@ -36,11 +32,8 @@ abbreviation { Faker::Lorem.unique.word } access { ResearchOutput.accesses.keys.sample } byte_size { Faker::Number.number } - coverage_end { Time.now + 2.years } - coverage_start { Time.now + 1.month } description { Faker::Lorem.paragraph } is_default { [nil, true, false].sample } - mandatory_attribution { Faker::Music::PearlJam.musician } display_order { Faker::Number.between(from: 1, to: 20) } output_type { ResearchOutput.output_types.keys.sample } output_type_description { Faker::Lorem.sentence } @@ -51,7 +44,6 @@ trait :complete do after(:create) do |research_output| - research_output.mime_type = create(:mime_type) # add a license identifier # add a repository identifier # add a metadata_standard identifier diff --git a/spec/models/api_client_spec.rb b/spec/models/api_client_spec.rb index b33c87d4ad..68fe7089f2 100644 --- a/spec/models/api_client_spec.rb +++ b/spec/models/api_client_spec.rb @@ -34,7 +34,6 @@ context "Associations" do it { is_expected.to belong_to(:org).optional } - it { is_expected.to have_many(:plans) } end context "Instance Methods" do diff --git a/spec/models/mime_type_spec.rb b/spec/models/mime_type_spec.rb deleted file mode 100644 index c6c826c0f1..0000000000 --- a/spec/models/mime_type_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require "rails_helper" - -RSpec.describe MimeType, type: :model do - - context "associations" do - it { is_expected.to have_many :research_outputs } - end - - context "validations" do - it { is_expected.to validate_presence_of(:category) } - it { is_expected.to validate_presence_of(:description) } - it { is_expected.to validate_presence_of(:value) } - end - - it "factory builds a valid model" do - expect(build(:mime_type).valid?).to eql(true) - end - - context "scopes" do - describe ":categories" do - before(:each) do - @categories = [Faker::Lorem.unique.word, Faker::Lorem.unique.word] - @categories.each { |category| 2.times { create(:mime_type, category: category) } } - @results = described_class.categories - end - - it "returns a unique list of categories" do - expect(@results.first).not_to eql(@results.last) - end - it "returns a sorted list of categories" do - expect(@results).to eql(@categories.sort { |a, b| a <=> b }) - end - end - end - -end diff --git a/spec/models/plan_spec.rb b/spec/models/plan_spec.rb index aedf054f2c..2f8f38c2de 100644 --- a/spec/models/plan_spec.rb +++ b/spec/models/plan_spec.rb @@ -4,6 +4,7 @@ describe Plan do + include IdentifierHelper include RolesHelper include TemplateHelper @@ -50,6 +51,8 @@ it { is_expected.to belong_to :org } + it { is_expected.to belong_to(:research_domain).optional } + it { is_expected.to belong_to(:funder).optional } it { is_expected.to have_many :phases } @@ -529,12 +532,11 @@ end - # TODO: Add this one in once we are able to easily do LEFT JOINs in Rails 5 context "when Contributor name matches term" do let!(:plan) { create(:plan, :creator, description: "foolike desc") } let!(:contributor) { create(:contributor, plan: plan, name: "Dr. Foo Bar") } - xit "returns contributor name" do + it "returns contributor name" do expect(subject).to include(plan) end end @@ -1542,7 +1544,6 @@ plan.grant = id plan.save plan2 = create(:plan, grant: id) - expect(plan2.grant).to eql(plan.grant) expect(plan2.grant.value).to eql(plan.grant.value) # Make sure that deleting the plan does not delete the shared grant! plan.destroy @@ -1550,4 +1551,32 @@ end end + describe "#grant association sanity checks" do + let!(:plan) { create(:plan, :creator) } + + it "allows a grant identifier to be associated" do + plan.grant = { value: build(:identifier, identifier_scheme: nil).value } + plan.save + expect(plan.grant.new_record?).to eql(false) + end + it "allows a grant identifier to be deleted" do + plan.grant = { value: build(:identifier, identifier_scheme: nil).value } + plan.save + plan.grant = { value: nil } + plan.save + expect(plan.grant).to eql(nil) + expect(Identifier.last).to eql(nil) + end + it "does not allow multiple grants on a single plan" do + plan.grant = { value: build(:identifier, identifier_scheme: nil).value } + plan.save + val = SecureRandom.uuid + plan.grant = { value: build(:identifier, identifier_scheme: nil, value: val).value } + plan.save + expect(plan.grant.new_record?).to eql(false) + expect(plan.grant.value).to eql(val) + expect(Identifier.all.length).to eql(1) + end + end + end diff --git a/spec/models/research_domain_spec.rb b/spec/models/research_domain_spec.rb new file mode 100644 index 0000000000..a9294fa69c --- /dev/null +++ b/spec/models/research_domain_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe ResearchDomain do + + context "associations" do + it { is_expected.to have_many :sub_fields } + it { is_expected.to belong_to(:parent).optional } + end + +end diff --git a/spec/models/research_output_spec.rb b/spec/models/research_output_spec.rb index 221f974fd5..d948f6c206 100644 --- a/spec/models/research_output_spec.rb +++ b/spec/models/research_output_spec.rb @@ -6,7 +6,6 @@ context "associations" do it { is_expected.to belong_to(:plan).optional } - it { is_expected.to belong_to(:mime_type).optional } end # rubocop:disable Layout/LineLength @@ -32,22 +31,6 @@ @subject.dataset! expect(@subject).not_to validate_presence_of(:output_type_description) end - - describe ":coverage_start and :coverage_end" do - it "allows coverage_start to be nil" do - @subject.coverage_start = nil - expect(@subject.valid?).to eql(true) - end - it "allows end_date to be nil" do - @subject.coverage_end = nil - expect(@subject.valid?).to eql(true) - end - it "does not allow end_date to come before start_date" do - @subject.coverage_end = Time.now - @subject.coverage_start = Time.now + 2.days - expect(@subject.valid?).to eql(false) - end - end end # rubocop:enable Layout/LineLength @@ -63,64 +46,9 @@ model.destroy expect(Plan.last).to eql(plan) end - it "does not delete associated mime_type" do - model = create(:research_output, :complete, plan: create(:plan)) - mime_type = model.mime_type - model.destroy - expect(MimeType.last).to eql(mime_type) - end end context "instance methods" do - describe ":available_mime_types" do - before(:each) do - @audiovisuals = %w[audio video].map do |cat| - create(:mime_type, category: cat) - end - @audiovisuals = @audiovisuals.sort { |a, b| a.description <=> b.description } - @images = [create(:mime_type, category: "image")] - @texts = [create(:mime_type, category: "text")] - @models = [create(:mime_type, category: "model")] - @subject = build(:research_output) - end - it "returns an empty array if no :output_type is present" do - @subject.output_type = nil - expect(@subject.available_mime_types.to_a).to eql([]) - end - it "returns an empty array if :output_type has no mime_types defined" do - @subject.physical_object! - expect(@subject.available_mime_types.to_a).to eql([]) - end - it "returns the correct mime_types for :output_type == :audiovisual" do - @subject.audiovisual! - expect(@subject.available_mime_types.to_a).to eql(@audiovisuals) - end - it "returns the correct mime_types for :output_type == :sound" do - @subject.sound! - expect(@subject.available_mime_types.to_a).to eql(@audiovisuals) - end - it "returns the correct mime_types for :output_type == :image" do - @subject.image! - expect(@subject.available_mime_types.to_a).to eql(@images) - end - it "returns the correct mime_types for :output_type == :data_paper" do - @subject.data_paper! - expect(@subject.available_mime_types.to_a).to eql(@texts) - end - it "returns the correct mime_types for :output_type == :dataset" do - @subject.dataset! - expect(@subject.available_mime_types.to_a).to eql(@texts) - end - it "returns the correct mime_types for :output_type == :text" do - @subject.text! - expect(@subject.available_mime_types.to_a).to eql(@texts) - end - it "returns the correct mime_types for :output_type == :model_representation" do - @subject.model_representation! - expect(@subject.available_mime_types.to_a).to eql(@models) - end - end - xit "licenses should have tests once implemented" do true end diff --git a/spec/presenters/api/v1/funding_presenter_spec.rb b/spec/presenters/api/v1/funding_presenter_spec.rb index feb6dde3d6..2216ace28c 100644 --- a/spec/presenters/api/v1/funding_presenter_spec.rb +++ b/spec/presenters/api/v1/funding_presenter_spec.rb @@ -1,21 +1,32 @@ # frozen_string_literal: true -require "rails_helper" +module Api -RSpec.describe Api::V1::FundingPresenter do + 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 - 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 plan's grant_number is nil" do - plan = build(:plan, grant_number: nil) - expect(described_class.status(plan: plan)).to eql("planned") - end - it "returns `granted` if the plan has a grant_number" do - plan = build(:plan, grant_number: Faker::Lorem.word) - expect(described_class.status(plan: plan)).to eql("granted") end + end end diff --git a/spec/presenters/api/v1/plan_presenter_spec.rb b/spec/presenters/api/v1/plan_presenter_spec.rb index ea5fb9071e..83ffb43180 100644 --- a/spec/presenters/api/v1/plan_presenter_spec.rb +++ b/spec/presenters/api/v1/plan_presenter_spec.rb @@ -25,7 +25,13 @@ expect(presenter.data_contact).to eql(nil) expect(presenter.contributors).to eql([]) end - it "sets data_contact" do + it "sets data_contact to the owner" do + # Plan.owner stupidly requires that the record be persisted, so need to create here + plan = create(:plan, :creator) + presenter = described_class.new(plan: plan) + expect(presenter.data_contact).to eql(plan.owner) + end + it "sets data_contact to the data_curation contributor if there is no owner" do expect(@presenter.data_contact).to eql(@data_contact) end it "sets other contributors (including the data_contact)" do diff --git a/spec/services/api/v1/contextual_error_service_spec.rb b/spec/services/api/v1/contextual_error_service_spec.rb index 235135557f..529b9f28c3 100644 --- a/spec/services/api/v1/contextual_error_service_spec.rb +++ b/spec/services/api/v1/contextual_error_service_spec.rb @@ -10,7 +10,7 @@ @plan.contributors << build(:contributor, org: build(:org), investigation: true) @plan.contributors.first.identifiers << build(:identifier) @plan.funder = build(:org) - @plan.grant = build(:identifier) + @plan.grant = { value: build(:identifier).value } end describe "process_plan_errors(plan:)" do @@ -38,9 +38,6 @@ it "contextualizes the :plan funder errors" do expect(@results.include?("Funding name can't be blank")).to eql(true) end - it "contextualizes the :plan grant errors" do - expect(@results.include?("Grant value can't be blank")).to eql(true) - end end describe "contextualize(errors:)" do @@ -106,13 +103,6 @@ expect(result.length).to eql(1) expect(result.first.start_with?("Funding name ")).to eql(true) end - it "returns errors if a Grant is invalid" do - @plan.grant.value = nil - @plan.grant.valid? - result = described_class.contextualize(errors: @plan.grant.errors, context: "Grant") - expect(result.length).to eql(1) - expect(result.first.start_with?("Grant value ")).to eql(true) - end end describe "valid_plan?(plan:)" do @@ -124,13 +114,9 @@ @plan.funder.name = nil expect(described_class.valid_plan?(plan: @plan)).to eql(false) end - it "return false if :plan grant is not valid" do - @plan.grant.value = nil - expect(described_class.valid_plan?(plan: @plan)).to eql(false) - end it "does not require :plan funder and grant to be present" do @plan.funder = nil - @plan.grant = nil + @plan.grant = {} expect(described_class.valid_plan?(plan: @plan)).to eql(true) end it "returns true when everything is valid" do diff --git a/spec/support/helpers/identifier_helper.rb b/spec/support/helpers/identifier_helper.rb new file mode 100644 index 0000000000..8a199a8a48 --- /dev/null +++ b/spec/support/helpers/identifier_helper.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module IdentifierHelper + + def create_orcid(user:, val: SecureRandom.uuid) + scheme = orcid_scheme + val = append_prefix(scheme: scheme, val: val) + create(:identifier, identifiable: user, identifier_scheme: scheme, value: val) + end + + def orcid_scheme + name = Rails.configuration.x.orcid.name || "orcid" + landing_page = Rails.configuration.x.orcid.landing_page_url || "https://orcid.org/" + scheme = IdentifierScheme.find_by(name: name) + scheme.update(identifier_prefix: landing_page) if scheme.present? + return scheme if scheme.present? + + create(:identifier_scheme, :for_identification, :for_users, name: name, + identifier_prefix: landing_page) + end + + def append_prefix(scheme:, val:) + val = val.start_with?("/") ? val[1..val.length] : val + url = landing_page_for(scheme: scheme) + val.start_with?(url) ? val : "#{url}#{val}" + end + + def remove_prefix(scheme:, val:) + val.gsub(landing_page_for(scheme: scheme), "") + end + + def landing_page_for(scheme:) + url = scheme.identifier_prefix + unless url.present? + url = Rails.configuration.x.send(:"#{scheme.name.downcase}")&.landing_page_url + end + return nil unless url.present? + + %w[/ : & ?].include?(url.last) ? url : "#{url}/" + end + +end diff --git a/spec/views/api/v1/plans/_funding.json.jbuilder_spec.rb b/spec/views/api/v1/plans/_funding.json.jbuilder_spec.rb index 3f200f6274..69dba26ac9 100644 --- a/spec/views/api/v1/plans/_funding.json.jbuilder_spec.rb +++ b/spec/views/api/v1/plans/_funding.json.jbuilder_spec.rb @@ -9,7 +9,9 @@ create(:identifier, identifiable: @funder, identifier_scheme: create(:identifier_scheme, name: "fundref")) @funder.reload - @plan = create(:plan, funder: @funder) + @plan = create(:plan, funder: @funder, org: create(:org), identifier: SecureRandom.uuid) + create(:identifier, identifiable: @plan.org, + identifier_scheme: create(:identifier_scheme, name: "ror")) @grant = create(:identifier, identifiable: @plan) @plan.update(grant_id: @grant.id) @plan.reload @@ -31,10 +33,23 @@ expect(@json[:funder_id][:type]).to eql(id.identifier_format) expect(@json[:funder_id][:identifier]).to eql(id.value) end + it "includes :dmproadmap_funding_opportunity_identifier" do + identifier = @plan.identifier + expect(@json[:dmproadmap_funding_opportunity_id][:type]).to eql("other") + expect(@json[:dmproadmap_funding_opportunity_id][:identifier]).to eql(identifier) + end it "includes :grant_ids" do expect(@json[:grant_id][:type]).to eql(@grant.identifier_format) expect(@json[:grant_id][:identifier]).to eql(@grant.value) end + it "includes :dmproadmap_funded_affiliations" do + org = @plan.org + expect(@json[:dmproadmap_funded_affiliations].any?).to eql(true) + affil = @json[:dmproadmap_funded_affiliations].last + expect(affil[:name]).to eql(org.name) + expect(affil[:affiliation_id][:type]).to eql(org.identifiers.last.identifier_format) + expect(affil[:affiliation_id][:identifier]).to eql(org.identifiers.last.value) + end end end diff --git a/spec/views/api/v1/plans/_show.json.jbuilder_spec.rb b/spec/views/api/v1/plans/_show.json.jbuilder_spec.rb index 2bf3068212..7f800c9d91 100644 --- a/spec/views/api/v1/plans/_show.json.jbuilder_spec.rb +++ b/spec/views/api/v1/plans/_show.json.jbuilder_spec.rb @@ -38,6 +38,16 @@ it "includes the :modified" do expect(@json[:modified]).to eql(@plan.updated_at.to_formatted_s(:iso8601)) end + it "includes :ethical_issues" do + expected = Api::V1::ConversionService.boolean_to_yes_no_unknown(@plan.ethical_issues) + expect(@json[:ethical_issues_exist]).to eql(expected) + end + it "includes :ethical_issues_description" do + expect(@json[:ethical_issues_description]).to eql(@plan.ethical_issues_description) + end + it "includes :ethical_issues_report" do + expect(@json[:ethical_issues_report]).to eql(@plan.ethical_issues_report) + end it "returns the URL of the plan as the :dmp_id if no DOI is defined" do expected = Rails.application.routes.url_helpers.api_v1_plan_url(@plan)