From 6876fb850948840afad74de1b1b5c75fc9c5c247 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 09:59:18 +0000 Subject: [PATCH 01/12] Add Author model and migration for authors has_many relationship Co-authored-by: fbacall <503373+fbacall@users.noreply.github.com> --- app/assets/javascripts/authors.js | 41 +++++++++++++++ app/controllers/materials_controller.rb | 3 +- app/models/author.rb | 10 ++++ app/models/material.rb | 18 +++++-- app/models/material_author.rb | 6 +++ app/views/common/_author_form.html.erb | 25 +++++++++ app/views/materials/_form.html.erb | 22 +++++++- db/migrate/20251119095244_create_authors.rb | 13 +++++ .../20251119095245_create_material_authors.rb | 12 +++++ ...119095246_migrate_material_authors_data.rb | 52 +++++++++++++++++++ ...119095247_remove_authors_from_materials.rb | 5 ++ test/fixtures/authors.yml | 26 ++++++++++ test/fixtures/material_authors.yml | 13 +++++ test/fixtures/materials.yml | 3 -- test/models/author_test.rb | 37 +++++++++++++ test/models/material_test.rb | 52 +++++++++++-------- 16 files changed, 306 insertions(+), 32 deletions(-) create mode 100644 app/assets/javascripts/authors.js create mode 100644 app/models/author.rb create mode 100644 app/models/material_author.rb create mode 100644 app/views/common/_author_form.html.erb create mode 100644 db/migrate/20251119095244_create_authors.rb create mode 100644 db/migrate/20251119095245_create_material_authors.rb create mode 100644 db/migrate/20251119095246_migrate_material_authors_data.rb create mode 100644 db/migrate/20251119095247_remove_authors_from_materials.rb create mode 100644 test/fixtures/authors.yml create mode 100644 test/fixtures/material_authors.yml create mode 100644 test/models/author_test.rb diff --git a/app/assets/javascripts/authors.js b/app/assets/javascripts/authors.js new file mode 100644 index 000000000..1ead1611c --- /dev/null +++ b/app/assets/javascripts/authors.js @@ -0,0 +1,41 @@ +var Authors = { + add: function (firstName, lastName, orcid) { + var newForm = $('#author-template').clone().html(); + + // Ensure the index of the new form is 1 greater than the current highest index, to prevent collisions + var index = 0; + $('#authors-list .author-form').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('#authors-list'); + + if (typeof firstName !== 'undefined' && typeof lastName !== 'undefined') { + $('.author-first-name', newForm).val(firstName); + $('.author-last-name', newForm).val(lastName); + if (typeof orcid !== 'undefined') { + $('.author-orcid', newForm).val(orcid); + } + } + + return false; // Stop form being submitted + }, + + // This is just cosmetic. The actual removal is done by rails, + // by virtue of the hidden checkbox being checked when the label is clicked. + delete: function () { + $(this).parents('.author-form').fadeOut(); + } +}; + +document.addEventListener("turbolinks:load", function() { + + $('#authors') + .on('click', '#add-author-btn', Authors.add) + .on('change', '.delete-author-btn input.destroy-attribute', Authors.delete); +}); diff --git a/app/controllers/materials_controller.rb b/app/controllers/materials_controller.rb index 4d9a54723..ff9930bee 100644 --- a/app/controllers/materials_controller.rb +++ b/app/controllers/materials_controller.rb @@ -171,11 +171,12 @@ 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: [] }, + { contributors: [] }, { target_audience: [] }, { collection_ids: [] }, { keywords: [] }, { resource_type: [] }, { scientific_topic_names: [] }, { scientific_topic_uris: [] }, { operation_names: [] }, { operation_uris: [] }, { node_ids: [] }, { node_names: [] }, { fields: [] }, + authors_attributes: %i[id first_name last_name orcid _destroy], external_resources_attributes: %i[id url title _destroy], external_resources: %i[url title], event_ids: [], locked_fields: []) diff --git a/app/models/author.rb b/app/models/author.rb new file mode 100644 index 000000000..c15d1ad29 --- /dev/null +++ b/app/models/author.rb @@ -0,0 +1,10 @@ +class Author < ApplicationRecord + has_many :material_authors, dependent: :destroy + has_many :materials, through: :material_authors + + validates :first_name, :last_name, presence: true + + def full_name + "#{first_name} #{last_name}".strip + end +end diff --git a/app/models/material.rb b/app/models/material.rb index 9e1726c08..8037526eb 100644 --- a/app/models/material.rb +++ b/app/models/material.rb @@ -30,7 +30,9 @@ class Material < ApplicationRecord text :description text :contact text :doi - text :authors + text :authors do + authors.map(&:full_name) + end text :contributors text :target_audience text :keywords @@ -51,7 +53,9 @@ class Material < ApplicationRecord end # other fields string :title - string :authors, multiple: true + string :authors, multiple: true do + authors.map(&:full_name) + end string :scientific_topics, multiple: true do scientific_topics_and_synonyms end @@ -102,6 +106,10 @@ class Material < ApplicationRecord has_many :stars, as: :resource, dependent: :destroy + has_many :material_authors, dependent: :destroy + has_many :authors, through: :material_authors + accepts_nested_attributes_for :authors, allow_destroy: true, reject_if: :all_blank + # 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 +119,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, :contributors, :target_audience, :resource_type, :subsets) - update_suggestions(:keywords, :contributors, :authors, :target_audience, + update_suggestions(:keywords, :contributors, :target_audience, :resource_type) def description=(desc) @@ -212,7 +220,7 @@ 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) } + authors.each { |a| xml.tag!('dc:creator', a.full_name) } contributors.each { |a| xml.tag!('dc:contributor', a) } xml.tag!('dc:publisher', content_provider.title) if content_provider diff --git a/app/models/material_author.rb b/app/models/material_author.rb new file mode 100644 index 000000000..b825001cd --- /dev/null +++ b/app/models/material_author.rb @@ -0,0 +1,6 @@ +class MaterialAuthor < ApplicationRecord + belongs_to :material + belongs_to :author + + validates :material, :author, presence: true +end diff --git a/app/views/common/_author_form.html.erb b/app/views/common/_author_form.html.erb new file mode 100644 index 000000000..40be68c78 --- /dev/null +++ b/app/views/common/_author_form.html.erb @@ -0,0 +1,25 @@ +<% index ||= 'replace-me' %> <%# This is so we can render a blank version of this sub-form in the page, %> + <%# which can be dynamically cloned using JavaScript to add more Authors to the main material form %> +<% field_name_prefix = "#{form_name}[authors_attributes][#{index}]" %> <%# This format is dictated by "accepts_nested_attributes_for" %> + +
diff --git a/app/views/materials/_form.html.erb b/app/views/materials/_form.html.erb index b71aac73a..fa13376d5 100644 --- a/app/views/materials/_form.html.erb +++ b/app/views/materials/_form.html.erb @@ -68,8 +68,22 @@ visibility_toggle: TeSS::Config.feature['materials_disabled'] %> - <%= f.multi_input :authors, suggestions_url: people_autocomplete_suggestions_path, title: t('materials.hints.authors'), - visibility_toggle: TeSS::Config.feature['materials_disabled'] %> +