Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 19, 2025

Summary of changes

  • Migrated Material.authors and Material.contributors from string arrays to proper has_many relationships with a new generic Person model containing given_name, family_name, full_name, and orcid fields
  • Created a reusable HasPeople concern with a has_person_role macro for DRY code that can be applied to any model and role
  • Created a polymorphic PersonLink join table with a role field, enabling reuse for different person roles (authors, contributors, etc.)
  • Added database migrations to create people and person_links tables, migrate existing data preserving full names, and remove the legacy columns
  • Updated controllers, views, and forms to support nested attributes for CRUD operations on people with different roles
  • Created a generic _person_form.html.erb partial that accepts a role parameter for reusability
  • Enhanced serializers and API responses to return structured person data with ORCID for both authors and contributors
  • Updated Bioschemas generator to include ORCID identifiers and separate given/family names in author and contributor metadata
  • Modified ingestors (Zenodo, CSV) to store author data as full_name without parsing
  • Added custom authors= and contributors= setters for backwards compatibility with legacy API clients (accepts both old and new field names)
  • Added optional belongs_to :profile association to Person model with automatic linking based on matching ORCID

Motivation and context

Enables proper author and contributor attribution with separate name fields and ORCID support for scholarly identification. The string arrays were limiting metadata quality and prevented linking to external author identifiers. The generic Person model, polymorphic PersonLink join table, and HasPeople concern allow for easy migration of any person-related fields on any model to the same structure. The automatic Profile linking via ORCID enables seamless integration between Person records and existing user profiles.

Key Implementation Details

HasPeople Concern

# Any model can now use:
class Material < ApplicationRecord
  include HasPeople
  
  has_person_role :authors, role_key: 'author'
  has_person_role :contributors, role_key: 'contributor'
end

# Automatically provides:
# - material.authors / material.contributors (scoped associations)
# - material.authors = [...] / material.contributors = [...] (custom setters)

Model Structure

# Person model (generic)
class Person < ApplicationRecord
  belongs_to :profile, optional: true
  has_many :person_links, dependent: :destroy
  
  # Validates that either full_name OR both given_name and family_name are present
  # Automatically links to Profile by matching ORCID on save
  
  def display_name
    full_name.presence || "#{given_name} #{family_name}".strip
  end
end

# PersonLink model (polymorphic join table)
class PersonLink < ApplicationRecord
  belongs_to :resource, polymorphic: true
  belongs_to :person
  validates :resource, :person, :role, presence: true
end

Person Model Fields

  • full_name - The complete name as it should be displayed (preferred for legacy data migration)
  • given_name - First/given name (optional if full_name is present)
  • family_name - Last/family name (optional if full_name is present)
  • orcid - ORCID identifier (optional)
  • profile_id - Optional link to a Profile (auto-populated based on matching ORCID)

Person-Profile Auto-Linking

When a Person is saved with an ORCID, it automatically links to a Profile with a matching ORCID:

person = Person.create(full_name: 'John Doe', orcid: '0000-0001-2345-6789')
person.profile  # => Will be set if a Profile exists with this ORCID

The ORCID matching handles both full URL format (https://orcid.org/...) and short ID format.

Legacy API Support

Custom setters for both authors= and contributors= accept multiple formats for backwards compatibility:

  • Array of strings (legacy format): ["John Doe", "Jane Smith"] - stored directly as full_name
  • Array of hashes: [{given_name: "John", family_name: "Doe", orcid: "..."}] or [{full_name: "Dr. Jane Marie Smith"}]
  • Array of Person objects: Direct Person model instances

Legacy field names (first_name, last_name) are also accepted and mapped to given_name/family_name.

UI Changes

<!-- Generic person form partial with role parameter -->
<%= render partial: 'common/person_form',
           locals: { form_name: 'material', person_link: person_link, role: 'author' } %>
<%= render partial: 'common/person_form',
           locals: { form_name: 'material', person_link: person_link, role: 'contributor' } %>

API Response Format

{
  "authors": [
    {
      "id": 1,
      "given_name": "Jane",
      "family_name": "Smith",
      "full_name": "Dr. Jane Marie Smith",
      "name": "Dr. Jane Marie Smith",
      "orcid": "0000-0002-1234-5678",
      "profile_id": 42
    }
  ],
  "contributors": [
    {
      "id": 2,
      "given_name": "John",
      "family_name": "Doe",
      "full_name": "John Doe",
      "name": "John Doe",
      "orcid": "0000-0001-2345-6789",
      "profile_id": null
    }
  ]
}

Bioschemas Enhancement

{
  "@type": "Person",
  "name": "Dr. Jane Marie Smith",
  "givenName": "Jane",
  "familyName": "Smith",
  "@id": "https://orcid.org/0000-0002-1234-5678"
}

Screenshots

N/A - Backend data model migration

Checklist

  • I have read and followed the CONTRIBUTING guide.
  • I confirm that I have the authority necessary to make this contribution on behalf of its copyright owner and agree to license it to the TeSS codebase under the BSD license.
Original prompt

Migrate the authors field of the Material model away from being just a simple array of strings to a has_many relationship linking to a new Author model, which has fields for the author's first_name, last_name, and orcid (all strings).


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Migrate authors field to has_many relationship Migrate Material authors from string array to has_many Author relationship Nov 19, 2025
Copilot AI requested a review from fbacall November 19, 2025 10:16
Copilot finished work on behalf of fbacall November 19, 2025 10:16
@fbacall
Copy link
Member

fbacall commented Dec 2, 2025

@copilot Change the new Author model to be a more generic People model. Make the join table and form also generic so it can be used for other fields, such as Material's contributors

Copy link
Contributor Author

Copilot AI commented Dec 2, 2025

@copilot Change the new Author model to be a more generic People model. Make the join table and form also generic so it can be used for other fields, such as Material's contributors

Refactored in 4dcfbb4. Changed Author model to generic Person model, and created polymorphic PersonLink join table with a role field. The form partial is now generic (_person_form.html.erb) and accepts a role parameter, making it reusable for authors, contributors, and other person-related fields.

Copilot AI changed the title Migrate Material authors from string array to has_many Author relationship Migrate Material authors from string array to has_many Person relationship Dec 2, 2025
Copilot finished work on behalf of fbacall December 2, 2025 10:30
@fbacall
Copy link
Member

fbacall commented Dec 2, 2025

@copilot We have legacy clients who will be creating Materials with authors through our API. Can you implement a custom author setter that will accept both an array of strings or an array of Person objects, converting the strings to Person objects?

Copy link
Contributor Author

Copilot AI commented Dec 2, 2025

@copilot We have legacy clients who will be creating Materials with authors through our API. Can you implement a custom author setter that will accept both an array of strings or an array of `Pe...

Implemented in 6afdfcf. Added a custom authors= setter that accepts:

  • Array of strings (legacy format): ["John Doe", "Jane Smith"] - parses into first/last names
  • Array of hashes: [{first_name: "John", last_name: "Doe", orcid: "..."}]
  • Array of Person objects

Single-name strings (e.g., "Madonna") are stored with an empty first_name and the name as last_name.

Copilot finished work on behalf of fbacall December 2, 2025 11:19
@fbacall
Copy link
Member

fbacall commented Dec 4, 2025

@copilot Do the same for Material contributors as you have done for authors. Keep the code DRY so it can easily be re-used for linking people to other roles, both on Material and other models.

Copy link
Contributor Author

Copilot AI commented Dec 4, 2025

@copilot Do the same for Material contributors as you have done for authors. Keep the code DRY so it can easily be re-used for linking people to other roles, both on Material and other models.

Implemented in 83487c0. Created a HasPeople concern with a has_person_role macro that can be reused on any model:

include HasPeople
has_person_role :authors, role_key: 'author'
has_person_role :contributors, role_key: 'contributor'

This creates both the scoped association and a custom setter for each role, accepting strings (legacy), hashes, or Person objects. Contributors now work identically to authors.

Copilot AI changed the title Migrate Material authors from string array to has_many Person relationship Migrate Material authors and contributors from string array to has_many Person relationship Dec 4, 2025
Copilot finished work on behalf of fbacall December 4, 2025 19:12
@fbacall
Copy link
Member

fbacall commented Dec 5, 2025

@copilot The data migration that splits a legacy string author name into first_name and last_name is error prone, e.g. some people have two first names, some cultures write their family name first...

Instead of doing this, lets add a full_name column to the people table which is displayed if present over first_name and last_name.

Additionally, change:

  • first_name to given_name
  • last_name to family_name

Copy link
Contributor Author

Copilot AI commented Dec 5, 2025

@copilot The data migration that splits a legacy string author name into first_name and last_name is error prone, e.g. some people have two first names, some cultures write their family name first...

Implemented in 044c720 and dddb034. Added a full_name column to the people table and renamed:

  • first_namegiven_name
  • last_namefamily_name

The display_name method returns full_name if present, otherwise constructs from given_name and family_name. The data migration now stores legacy names directly in full_name without parsing. The custom setters also accept legacy first_name/last_name field names for backwards compatibility with existing API clients.

Copilot finished work on behalf of fbacall December 5, 2025 11:29
@fbacall
Copy link
Member

fbacall commented Dec 5, 2025

@copilot Add an optional belongs_to to Person that allows it to be linked to a Profile. Automatically make this connection on save using the orcid of both tables.

Copy link
Contributor Author

Copilot AI commented Dec 5, 2025

@copilot Add an optional belongs_to to Person that allows it to be linked to a Profile. Automatically make this connection on save using the orcid of both tables.

Implemented in ea22c13. Added an optional belongs_to :profile association to the Person model. A before_save callback automatically links the Person to a Profile if one exists with a matching ORCID. The matching handles both full URL format (https://orcid.org/...) and short ID format. Existing profile links are not overridden.

Copilot finished work on behalf of fbacall December 5, 2025 14:27
Co-authored-by: fbacall <503373+fbacall@users.noreply.github.com>
Copilot AI and others added 11 commits December 5, 2025 15:35
Co-authored-by: fbacall <503373+fbacall@users.noreply.github.com>
Co-authored-by: fbacall <503373+fbacall@users.noreply.github.com>
Co-authored-by: fbacall <503373+fbacall@users.noreply.github.com>
Co-authored-by: fbacall <503373+fbacall@users.noreply.github.com>
Co-authored-by: fbacall <503373+fbacall@users.noreply.github.com>
…y_name

Co-authored-by: fbacall <503373+fbacall@users.noreply.github.com>
… compatibility

Co-authored-by: fbacall <503373+fbacall@users.noreply.github.com>
Co-authored-by: fbacall <503373+fbacall@users.noreply.github.com>
@fbacall fbacall force-pushed the copilot/migrate-authors-to-has-many branch from ea22c13 to 8125c8b Compare December 5, 2025 15:43
@fbacall
Copy link
Member

fbacall commented Dec 5, 2025

TODO

  • Fix tests
  • Remove the person_link join table, do everything on Person. Just do it like AssetsCreators in SEEK
  • Make sure the setter does not needlessly destroy/remake unchanged people when the edit form is submitted
  • Decide what to do about API
  • Update RDF extractors

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants