diff --git a/Gemfile.lock b/Gemfile.lock index 613cdaf34..29ed7b3d3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ GIT remote: https://github.com/ElixirTeSS/TeSS_RDF_Extractors - revision: f9eee4621738c8fcaa9e0729e76a307fcd55650f + revision: 135ce24ef909bf5f7c6e14ccb1e404b51478ab07 branch: master specs: - tess_rdf_extractors (1.1.0) + tess_rdf_extractors (1.2.0) linkeddata (~> 3.2.0) GEM @@ -97,8 +97,8 @@ GEM minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + addressable (2.8.9) + public_suffix (>= 2.0.2, < 8.0) aes_key_wrap (1.1.0) ahoy_matey (4.2.1) activesupport (>= 5.2) @@ -119,7 +119,7 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (3.2.3) + bigdecimal (3.3.1) bindata (2.5.0) bindex (0.8.1) binding_of_caller (1.0.0) @@ -147,8 +147,8 @@ GEM json_schema (~> 0.14, >= 0.14.3) openapi_parser (~> 1.0) rack (>= 1.5) - concurrent-ruby (1.3.5) - connection_pool (2.5.4) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) countries (5.6.0) unaccent (~> 0.3) country_select (8.0.2) @@ -157,7 +157,7 @@ GEM rexml crass (1.0.6) csv (3.3.2) - date (3.4.1) + date (3.5.1) debug_inspector (1.1.0) device_detector (1.1.1) devise (4.9.4) @@ -206,7 +206,7 @@ GEM globalid (1.2.1) activesupport (>= 6.1) gravtastic (3.2.6) - haml (6.3.0) + haml (6.4.0) temple (>= 0.8.2) thor tilt @@ -218,7 +218,7 @@ GEM tilt (>= 1.2) hashdiff (1.0.1) hashie (5.0.0) - htmlentities (4.3.4) + htmlentities (4.4.2) http-accept (1.7.0) http-cookie (1.0.5) domain_name (~> 0.5) @@ -233,7 +233,7 @@ GEM icalendar (2.9.0) ice_cube (~> 0.16) ice_cube (0.16.4) - io-console (0.8.1) + io-console (0.8.2) irb (1.15.2) pp (>= 0.6.0) rdoc (>= 4.0.0) @@ -365,15 +365,15 @@ GEM money (~> 6.13) railties (>= 3.0) msgpack (1.7.2) - multi_json (1.17.0) + multi_json (1.19.1) multi_xml (0.8.0) bigdecimal (>= 3.1, < 5) multipart-post (2.4.1) nested_form (0.3.2) net-http (0.9.1) uri (>= 0.11.1) - net-http-persistent (4.0.6) - connection_pool (~> 2.2, >= 2.2.4) + net-http-persistent (4.0.8) + connection_pool (>= 2.2.4, < 4) net-imap (0.5.8) date net-protocol @@ -436,7 +436,7 @@ GEM pry-byebug (3.10.1) byebug (~> 11.0) pry (>= 0.13, < 0.15) - psych (5.2.6) + psych (5.3.1) date stringio public_activity (3.0.1) @@ -444,7 +444,7 @@ GEM activerecord (>= 6.1) i18n (>= 0.5.0) railties (>= 6.1.0) - public_suffix (6.0.2) + public_suffix (7.0.5) puma (6.5.0) nio4r (~> 2.0) pundit (2.3.1) @@ -599,7 +599,7 @@ GEM redis-client (0.16.0) connection_pool regexp_parser (2.8.1) - reline (0.6.2) + reline (0.6.3) io-console (~> 0.5) responders (3.1.1) actionpack (>= 5.2) @@ -611,7 +611,7 @@ GEM netrc (~> 0.8) reverse_markdown (2.1.1) nokogiri - rexml (3.4.2) + rexml (3.4.4) rouge (4.1.3) rsolr (2.6.0) builder (>= 2.1.2) @@ -729,7 +729,7 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - stringio (3.1.7) + stringio (3.2.0) sunspot (2.7.1) bigdecimal pr_geohash (~> 1.0) @@ -750,8 +750,8 @@ GEM climate_control (>= 0.0.3, < 1.0) terser (1.1.17) execjs (>= 0.3.0, < 3) - thor (1.4.0) - tilt (2.6.1) + thor (1.5.0) + tilt (2.7.0) timeout (0.4.3) tsort (0.2.0) turbo-rails (2.0.11) @@ -771,7 +771,7 @@ GEM unf_ext unf_ext (0.0.8.2) unicode-display_width (2.4.2) - unicode-types (1.10.0) + unicode-types (1.11.0) uri (1.1.1) useragent (0.16.11) validate_email (0.1.6) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 90b513d49..9718183a2 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -276,6 +276,8 @@ document.addEventListener("turbolinks:load", function(e) { LearningPaths.init(); + People.init(); + $('.tess-expandable').each(function () { if (this.dataset.origHeight) { return; diff --git a/app/assets/javascripts/people.js b/app/assets/javascripts/people.js new file mode 100644 index 000000000..752d70188 --- /dev/null +++ b/app/assets/javascripts/people.js @@ -0,0 +1,77 @@ +const People = { + add: function (template, list) { + let newForm = template.clone().html(); + // Ensure the index of the new form is 1 greater than the current highest index, to prevent collisions + let index = 0; + $('.person-form', list).each(function () { + var newIndex = parseInt($(this).data('index')); + if (newIndex > index) { + index = newIndex; + } + }); + + // Replace the placeholder index with the actual index + newForm = $(newForm.replace(/replace-me/g, index + 1)); + newForm.appendTo(list); + const nameInput = newForm.find('.person-name'); + const orcidInput = newForm.find('.person-orcid'); + const opts = { + orientation: 'top', + triggerSelectOnValidInput: false, + onSelect: function (suggestion) { + orcidInput.val(suggestion.data.orcid); + }, + transformResult: function(response) { + return { + suggestions: $.map(response.suggestions, function(item) { + item.data.hint = item.data.orcid; + return item; + }) + }; + }, + formatResult: Autocompleters.formatResultWithHint + } + + opts.serviceUrl = list.parents('[data-role="people-form"]').data('autocompleteUrl'); + opts.dataType = 'json'; + opts.deferRequestBy = 100; + + nameInput.autocomplete(opts); + + return newForm; + }, + + delete: function () { + $(this).parents('.person-form').fadeOut('fast', function() { + $(this).remove(); + }); + }, + + init: function () { + $('[data-role="people-form"]').each(function () { + const form = $(this); + const template = form.find('[data-role="people-form-template"]'); + const list = form.find('[data-role="people-form-list"]'); + + form.find('[data-role="people-form-add"]').click(function (e) { + e.preventDefault(); + const nextItem = People.add(template, list); + nextItem.find('input.form-control:first').focus(); + }); + + // Add new person if enter is pressed on final person, otherwise focus the next person in the list. + $(form).on('keyup', 'input', function (e) { + if (e.which === 13) { + e.preventDefault(); + let nextItem = $(e.target).parents('.person-form').next('.person-form'); + if (!nextItem.length) { + nextItem = People.add(template, list); + } + nextItem.find('input.form-control:first').focus(); + } + }); + }); + + $('.delete-person-btn input.destroy-attribute').change(People.delete); + } +}; diff --git a/app/assets/stylesheets/external-resources.scss b/app/assets/stylesheets/external-resources.scss index 839776636..f14d3bbea 100644 --- a/app/assets/stylesheets/external-resources.scss +++ b/app/assets/stylesheets/external-resources.scss @@ -1,13 +1,3 @@ -.external-resource-form { - padding-bottom: 10px; - margin-bottom: 10px; - - &.deleted { - color: $brand-danger; - text-decoration: line-through; - } -} - .associate-resource { cursor: pointer; } diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index 42685528f..a1c0cc9db 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -36,7 +36,17 @@ label.required abbr { margin-top: 0; } -.delete-external-resource-btn { +.nested-resource-form { + padding-bottom: 10px; + margin-bottom: 10px; + + &.deleted { + color: $brand-danger; + text-decoration: line-through; + } +} + +.delete-nested-resource-btn { @include delete-cross; cursor: pointer; diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 309a8bccb..9d316890c 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -7,7 +7,15 @@ def suggestions end def people_suggestions - suggestions = AutocompleteSuggestion.people.query(params[:query]) + people = Person.query(params[:query]) + suggestions = people.map do |p| + { value: p.name, + data: { + orcid: p.orcid, + profile_id: p.profile_id + } + } + end respond_with({ suggestions: suggestions }) end end diff --git a/app/controllers/learning_paths_controller.rb b/app/controllers/learning_paths_controller.rb index 24d85d9f9..1e1db7411 100644 --- a/app/controllers/learning_paths_controller.rb +++ b/app/controllers/learning_paths_controller.rb @@ -103,11 +103,14 @@ def learning_path_params params.require(:learning_path).permit(:id, :title, :description, :licence, :doi, :content_provider_id, :difficulty_level, :status, :prerequisites, :syllabus, :learning_objectives, - { contributors: [] }, { authors: [] }, { target_audience: [] }, + { target_audience: [] }, { keywords: [] }, { scientific_topic_names: [] }, { scientific_topic_uris: [] }, { node_ids: [] }, { node_names: [] }, - { topic_links_attributes: [:id, :topic_id, :order, :_destroy] }, :public) + { topic_links_attributes: [:id, :topic_id, :order, :_destroy] }, :public, + { authors: [:name, :orcid] }, { contributors: [:name, :orcid] }, # Structured + { authors: [] }, { contributors: [] } # as strings + ) end end diff --git a/app/controllers/materials_controller.rb b/app/controllers/materials_controller.rb index 72806976d..c70360f89 100644 --- a/app/controllers/materials_controller.rb +++ b/app/controllers/materials_controller.rb @@ -171,11 +171,13 @@ def material_params :content_provider_id, :difficulty_level, :version, :status, :date_created, :date_modified, :date_published, :other_types, :prerequisites, :syllabus, :visible, :learning_objectives, { subsets: [] }, - { contributors: [] }, { authors: [] }, { target_audience: [] }, + { target_audience: [] }, { collection_ids: [] }, { keywords: [] }, { resource_type: [] }, { scientific_topic_names: [] }, { scientific_topic_uris: [] }, { operation_names: [] }, { operation_uris: [] }, { node_ids: [] }, { node_names: [] }, { fields: [] }, + { authors: [:name, :orcid] }, { contributors: [:name, :orcid] }, # Structured + { authors: [] }, { contributors: [] }, # as strings external_resources_attributes: %i[id url title _destroy], external_resources: %i[url title], event_ids: [], locked_fields: []) diff --git a/app/controllers/workflows_controller.rb b/app/controllers/workflows_controller.rb index 2af2055de..65acedfcf 100644 --- a/app/controllers/workflows_controller.rb +++ b/app/controllers/workflows_controller.rb @@ -117,8 +117,11 @@ def workflow_params params.require(:workflow).permit(:title, :description, :user_id, :workflow_content, :doi, :remote_created_date, :remote_updated_date, { keywords: [] }, { scientific_topic_names: [] }, { scientific_topic_uris: [] }, :licence, - :difficulty_level, { contributors: [] }, { authors: [] }, { target_audience: [] }, - :hide_child_nodes, :public) + :difficulty_level, { target_audience: [] }, + :hide_child_nodes, :public, + { authors: [:name, :orcid] }, { contributors: [:name, :orcid] }, # Structured + { authors: [] }, { contributors: [] } # as strings + ) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 79db6327a..8c1ef2d02 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -714,4 +714,27 @@ def per_page_options_for_select options_for_select(SearchableIndex::PER_PAGE_OPTIONS.map { |k| [k, k] }, params[:per_page].presence || SearchableIndex::DEFAULT_PAGE_SIZE) end + + def attributes_to_hidden_fields(param_key, attrs) + flatten_params(param_key, attrs).map do |name, value| + hidden_field_tag(name, value) + end.join.html_safe + end + + private + + def flatten_params(prefix, value) + case value + when Hash + value.flat_map do |k, v| + flatten_params("#{prefix}[#{k}]", v) + end + when Array + value.flat_map do |v| + flatten_params("#{prefix}[]", v) + end + else + [[prefix, value]] + end + end end diff --git a/app/helpers/materials_helper.rb b/app/helpers/materials_helper.rb index 9ccd964d3..0fc4d362e 100644 --- a/app/helpers/materials_helper.rb +++ b/app/helpers/materials_helper.rb @@ -103,6 +103,22 @@ def display_attribute(resource, attribute, show_label: true, title: nil, markdow string.html_safe end + def display_people(resource, attribute) + display_attribute(resource, attribute) do |values| + html = values.map do |person| + if person.profile + link_to(person.profile.full_name, person.profile) + elsif person.orcid.present? + image_tag('ORCID-iD_icon_vector.svg', size: 16) + ' ' + + external_link(person.display_name, person.orcid_url) + else + person.display_name + end + end + safe_join(html, ', ') + end + end + def display_attribute_no_label(resource, attribute, markdown: false, &block) # resource e.g. <#Material> & symbol e.g. :target_audience display_attribute(resource, attribute, markdown:, show_label: false, &block) end diff --git a/app/models/concerns/has_orcid.rb b/app/models/concerns/has_orcid.rb new file mode 100644 index 000000000..6d7e6be24 --- /dev/null +++ b/app/models/concerns/has_orcid.rb @@ -0,0 +1,19 @@ +module HasOrcid + extend ActiveSupport::Concern + + included do + auto_strip_attributes :orcid + before_validation :normalize_orcid + validates :orcid, orcid: true, allow_blank: true + end + + def orcid_url + return nil if orcid.blank? + OrcidValidator.orcid_url(orcid) + end + + def normalize_orcid + return if orcid.blank? + self.orcid = orcid.strip.sub(OrcidValidator::ORCID_DOMAIN_REGEX, '') + end +end diff --git a/app/models/concerns/has_people.rb b/app/models/concerns/has_people.rb new file mode 100644 index 000000000..88120f6c9 --- /dev/null +++ b/app/models/concerns/has_people.rb @@ -0,0 +1,60 @@ +module HasPeople + VALID_ATTRS = [:name, :orcid, :profile_id].freeze + extend ActiveSupport::Concern + + class_methods do + # Define a person role association (e.g., :authors, :contributors) + # This creates the association and a custom setter that accepts strings, hashes, or Person objects + def has_person_role(role_name) + role_key = role_name.to_s.singularize + # Define the association + has_many role_name, -> { where(role: role_key) }, class_name: 'Person', as: :resource, inverse_of: :resource, + autosave: true, dependent: :destroy + + # Define custom setter that accepts strings (legacy), hashes, or Person objects + define_method("#{role_name}=") do |value| + super(set_people_for_role(value, role_name, role_key)) + end + end + end + + private + + # Set people for a specific role, accepting various input formats + def set_people_for_role(value, role_name, role_key) + send(role_name).reset + current_people = send(role_name).to_a + to_keep = [] + + value = value.values if value.respond_to?(:keys) && value.keys.first&.match(/\A\d+\z/) + Array.wrap(value).reject(&:blank?).map do |person_data| + person_data = person_data.to_h if person_data.is_a?(ActionController::Parameters) + if person_data.is_a?(String) + attrs = { name: person_data.strip } + elsif person_data.is_a?(Hash) + attrs = person_data.with_indifferent_access.slice(*VALID_ATTRS) + elsif person_data.is_a?(Person) + attrs = person_data.attributes.with_indifferent_access.slice(*VALID_ATTRS) + end + + idx = current_people.index { |p| (p.orcid.present? && p.orcid == attrs[:orcid]) || p.name == attrs[:name] } + if idx + match = current_people.delete_at(idx) + match.assign_attributes(**attrs, role: role_key) + to_keep << match + else + if person_data.is_a?(Person) + person = person_data + person.role = role_key + else + person = send(role_name).build(**attrs) + end + to_keep << person + end + end + + current_people.each(&:mark_for_destruction) # Now contains only redundant records + + to_keep + end +end diff --git a/app/models/learning_path.rb b/app/models/learning_path.rb index cafc70476..938b6ab52 100644 --- a/app/models/learning_path.rb +++ b/app/models/learning_path.rb @@ -10,6 +10,7 @@ class LearningPath < ApplicationRecord include HasSuggestions include Collaboratable include InSpace + include HasPeople if TeSS::Config.solr_enabled # :nocov: @@ -17,8 +18,12 @@ class LearningPath < ApplicationRecord # full text search fields text :title text :description - text :authors - text :contributors + text :authors do + authors.map(&:display_name) + end + text :contributors do + contributors.map(&:display_name) + end text :target_audience text :keywords text :learning_path_type @@ -31,13 +36,17 @@ class LearningPath < ApplicationRecord end # other fields string :title - string :authors, :multiple => true + string :authors, multiple: true do + authors.map(&:display_name) + end string :scientific_topics, :multiple => true do self.scientific_topic_names end string :target_audience, :multiple => true string :keywords, :multiple => true - string :contributors, :multiple => true + string :contributors, multiple: true do + contributors.map(&:display_name) + end string :content_provider do self.content_provider.try(:title) end @@ -78,11 +87,14 @@ class LearningPath < ApplicationRecord validates :title, :description, presence: true validates :keywords, length: { maximum: 20 } - clean_array_fields(:keywords, :contributors, :authors, :target_audience) - update_suggestions(:keywords, :contributors, :authors, :target_audience) + clean_array_fields(:keywords, :target_audience) + update_suggestions(:keywords, :target_audience) accepts_nested_attributes_for :topic_links, allow_destroy: true + has_person_role :authors + has_person_role :contributors + def description= desc super(Rails::Html::FullSanitizer.new.sanitize(desc)) end diff --git a/app/models/material.rb b/app/models/material.rb index 40e7fc83e..9663b35fb 100644 --- a/app/models/material.rb +++ b/app/models/material.rb @@ -21,6 +21,7 @@ class Material < ApplicationRecord include HasDifficultyLevel include HasEdamTerms include InSpace + include HasPeople if TeSS::Config.solr_enabled # :nocov: @@ -30,8 +31,12 @@ class Material < ApplicationRecord text :description text :contact text :doi - text :authors - text :contributors + text :authors do + authors.map(&:display_name) + end + text :contributors do + contributors.map(&:display_name) + end text :target_audience text :keywords text :resource_type @@ -51,7 +56,9 @@ class Material < ApplicationRecord end # other fields string :title - string :authors, multiple: true + string :authors, multiple: true do + authors.map(&:display_name) + end string :scientific_topics, multiple: true do scientific_topics_and_synonyms end @@ -62,7 +69,9 @@ class Material < ApplicationRecord string :keywords, multiple: true string :fields, multiple: true string :resource_type, multiple: true - string :contributors, multiple: true + string :contributors, multiple: true do + contributors.map(&:display_name) + end string :content_provider do content_provider.try(:title) end @@ -102,6 +111,10 @@ class Material < ApplicationRecord has_many :stars, as: :resource, dependent: :destroy + # Use HasPeople concern for authors and contributors + has_person_role :authors + has_person_role :contributors + # Remove trailing and squeezes (:squish option) white spaces inside the string (before_validation): # e.g. "James Bond " => "James Bond" auto_strip_attributes :title, :description, :url, squish: false @@ -111,10 +124,10 @@ class Material < ApplicationRecord validates :other_types, presence: true, if: proc { |m| m.resource_type.include?('other') } validates :keywords, length: { maximum: 20 } - clean_array_fields(:keywords, :fields, :contributors, :authors, + clean_array_fields(:keywords, :fields, :target_audience, :resource_type, :subsets) - update_suggestions(:keywords, :contributors, :authors, :target_audience, + update_suggestions(:keywords, :target_audience, :resource_type) def description=(desc) @@ -212,8 +225,8 @@ def to_oai_dc 'xsi:schemaLocation' => 'http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd') do xml.tag!('dc:title', title) xml.tag!('dc:description', description) - authors.each { |a| xml.tag!('dc:creator', a) } - contributors.each { |a| xml.tag!('dc:contributor', a) } + authors.each { |a| xml.tag!('dc:creator', a.display_name) } + contributors.each { |c| xml.tag!('dc:contributor', c.display_name) } xml.tag!('dc:publisher', content_provider.title) if content_provider xml.tag!('dc:format', 'text/html') diff --git a/app/models/person.rb b/app/models/person.rb new file mode 100644 index 000000000..039ed94b3 --- /dev/null +++ b/app/models/person.rb @@ -0,0 +1,38 @@ +class Person < ApplicationRecord + include HasOrcid + + belongs_to :profile, optional: true + belongs_to :resource, polymorphic: true + + validates :resource, :role, :name, presence: true + + # Automatically link to profile based on ORCID on save + before_save :link_to_profile_by_orcid + + # Return the display name - currently just the full name + def display_name + name + end + + # For autocomplete + def self.starting_with(query) + where('lower(name) LIKE ?', "#{query.downcase}%") + end + + def self.query(query, limit = nil) + q = select(:name, :orcid, :profile_id).starting_with(query).distinct + q = q.limit(limit) if limit + q.order(name: :asc, orcid: :asc) + end + + private + + # Automatically link to a Profile if one exists with a matching ORCID, or unlink if no match. + def link_to_profile_by_orcid + if orcid.blank? + self.profile = nil + else + self.profile = Profile.find_by(orcid: orcid, orcid_authenticated: true) + end + end +end diff --git a/app/models/profile.rb b/app/models/profile.rb index 9935bcca9..b09f98925 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -1,13 +1,14 @@ require 'uri' class Profile < ApplicationRecord - auto_strip_attributes :firstname, :surname, :website, :orcid, squish: false + include HasOrcid + + auto_strip_attributes :firstname, :surname, :website, squish: false belongs_to :user, inverse_of: :profile - before_validation :normalize_orcid validates :firstname, :surname, :description, presence: true, if: :public? validates :website, url: true, http_url: { allow_inaccessible: true }, allow_blank: true - validates :orcid, orcid: true, allow_blank: true + after_validation :check_public after_commit :reindex_trainer, on: %i[create update] clean_array_fields(:expertise_academic, :expertise_technical, :fields, @@ -25,11 +26,6 @@ def full_name "#{firstname} #{surname}".strip end - def orcid_url - return nil if orcid.blank? - "#{OrcidValidator::ORCID_PREFIX}#{orcid}" - end - def merge(*others) Profile.transaction do attrs = attributes @@ -49,6 +45,7 @@ def merge(*others) end def authenticate_orcid(orcid) + old_orcid = self.orcid existing = Profile.where(orcid: orcid, orcid_authenticated: true) self.orcid = orcid self.orcid_authenticated = true @@ -59,6 +56,7 @@ def authenticate_orcid(orcid) next if profile == self profile.update_column(:orcid_authenticated, false) end + PersonLinkWorker.perform_async([old_orcid, orcid].compact_blank) end out @@ -66,11 +64,6 @@ def authenticate_orcid(orcid) private - def normalize_orcid - return if orcid.blank? - self.orcid = orcid.strip.sub(OrcidValidator::ORCID_DOMAIN_REGEX, '') - end - def check_public public ? self.type = 'Trainer' : self.type = 'Profile' end diff --git a/app/models/workflow.rb b/app/models/workflow.rb index f32762fbe..bd135facc 100644 --- a/app/models/workflow.rb +++ b/app/models/workflow.rb @@ -12,6 +12,7 @@ class Workflow < ApplicationRecord include HasDifficultyLevel include HasEdamTerms include InSpace + include HasPeople if TeSS::Config.solr_enabled # :nocov: @@ -29,11 +30,18 @@ class Workflow < ApplicationRecord text :node_descriptions do node_index('description') end - text :authors + text :authors do + authors.map(&:display_name) + end + text :contributors do + contributors.map(&:display_name) + end text :scientific_topics do scientific_topics_and_synonyms end - string :authors, :multiple => true + string :authors, multiple: true do + authors.map(&:display_name) + end string :scientific_topics, :multiple => true do scientific_topics_and_synonyms end @@ -41,8 +49,9 @@ class Workflow < ApplicationRecord string :target_audience, :multiple => true text :keywords string :keywords, :multiple => true - text :contributors - string :contributors, :multiple => true + string :contributors, multiple: true do + contributors.map(&:display_name) + end integer :user_id boolean :public @@ -65,12 +74,15 @@ class Workflow < ApplicationRecord validates :title, presence: true validates :keywords, length: { maximum: 20 } - clean_array_fields(:keywords, :contributors, :authors, :target_audience) + clean_array_fields(:keywords, :target_audience) - update_suggestions(:keywords, :contributors, :authors, :target_audience) + update_suggestions(:keywords, :target_audience) after_update :log_diagram_modification + has_person_role :authors + has_person_role :contributors + def self.facet_fields %w(scientific_topics target_audience keywords licence difficulty_level authors contributors) end diff --git a/app/serializers/application_serializer.rb b/app/serializers/application_serializer.rb index 2837c33ab..b7480c659 100644 --- a/app/serializers/application_serializer.rb +++ b/app/serializers/application_serializer.rb @@ -31,5 +31,9 @@ def ontology_terms(type) object.send(type).map { |t| { preferred_label: t.preferred_label, uri: t.uri } } end + def people(type) + object.send(type).map(&:display_name) + end + link(:self) { polymorphic_path(object) } end \ No newline at end of file diff --git a/app/serializers/material_serializer.rb b/app/serializers/material_serializer.rb index 4500fe153..548c37714 100644 --- a/app/serializers/material_serializer.rb +++ b/app/serializers/material_serializer.rb @@ -18,4 +18,12 @@ class MaterialSerializer < ApplicationSerializer has_many :nodes has_many :collections has_many :events + + def contributors + people(:contributors) + end + + def authors + people(:authors) + end end diff --git a/app/serializers/workflow_serializer.rb b/app/serializers/workflow_serializer.rb index dd716f985..b212a4959 100644 --- a/app/serializers/workflow_serializer.rb +++ b/app/serializers/workflow_serializer.rb @@ -5,4 +5,12 @@ class WorkflowSerializer < ApplicationSerializer :created_at, :updated_at belongs_to :user + + def contributors + people(:contributors) + end + + def authors + people(:authors) + end end diff --git a/app/validators/orcid_validator.rb b/app/validators/orcid_validator.rb index f4349e2ec..311d3e28f 100644 --- a/app/validators/orcid_validator.rb +++ b/app/validators/orcid_validator.rb @@ -8,6 +8,10 @@ def validate_each(record, attribute, value) record.errors.add(attribute, options[:message] || "isn't a valid ORCID identifier") end + def self.orcid_url(orcid) + "#{ORCID_PREFIX}#{orcid}" + end + private # checks the structure of the id, and whether is conforms to ISO/IEC 7064:2003 diff --git a/app/views/bioschemas/_preview_button.html.erb b/app/views/bioschemas/_preview_button.html.erb index 45eb032e8..ebfb09c62 100644 --- a/app/views/bioschemas/_preview_button.html.erb +++ b/app/views/bioschemas/_preview_button.html.erb @@ -1,13 +1,5 @@ -<%= form_for(resource, url: polymorphic_path([:preview, resource.class])) do |f| %> - <% resource.attributes.each do |key, value| %> - <%= f.hidden_field(key, multiple: value.is_a?(Array)) %> - <% end %> -
+<%= form_tag(polymorphic_path([:preview, resource.class])) do |f| %> + <%= attributes_to_hidden_fields(resource.class.model_name.param_key, resource_params) %> - <%= f.submit('Preview', class: 'btn btn-primary', formtarget: '_blank', data: { disable_with: false }) %> + <%= submit_tag('Preview', class: 'btn btn-primary', formtarget: '_blank', data: { disable_with: false }) %> <% end %> diff --git a/app/views/bioschemas/_test_results.html.erb b/app/views/bioschemas/_test_results.html.erb index 620e8126f..43528aada 100644 --- a/app/views/bioschemas/_test_results.html.erb +++ b/app/views/bioschemas/_test_results.html.erb @@ -58,7 +58,7 @@ <% end %> <% sample.each do |resource_params| %> <% resource = Source.get_test_resource(type, resource_params, user: User.get_default_user) %> - <%= render partial: 'sources/test_resource', locals: { resource: resource } %> + <%= render partial: 'sources/test_resource', locals: { resource: resource, resource_params: resource_params } %> <% end %> <% end %> diff --git a/app/views/common/_external_resource_form.html.erb b/app/views/common/_external_resource_form.html.erb index 19d93a0c9..c276607cf 100644 --- a/app/views/common/_external_resource_form.html.erb +++ b/app/views/common/_external_resource_form.html.erb @@ -2,7 +2,7 @@ <%# which can be dynamically cloned using JavaScript to add more ExternalResources to the main material form %> <% field_name_prefix = "#{form_name}[external_resources_attributes][#{index}]" %> <%# This format is dictated by "accepts_nested_attributes_for" %> -