Skip to content

Commit ebf99c2

Browse files
Copilotfbacall
andcommitted
Add optional belongs_to :profile on Person, auto-link by ORCID on save
Co-authored-by: fbacall <503373+fbacall@users.noreply.github.com>
1 parent dc0606a commit ebf99c2

File tree

5 files changed

+64
-3
lines changed

5 files changed

+64
-3
lines changed

app/models/person.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
class Person < ApplicationRecord
2+
belongs_to :profile, optional: true
23
has_many :person_links, dependent: :destroy
34

45
# Validate that at least a full_name OR both given_name and family_name are present
56
validate :name_presence
67

8+
# Automatically link to profile based on ORCID on save
9+
before_save :link_to_profile_by_orcid
10+
711
# Return the display name - full_name if present, otherwise construct from given_name and family_name
812
def display_name
913
full_name.presence || "#{given_name} #{family_name}".strip
@@ -16,4 +20,19 @@ def name_presence
1620
errors.add(:base, "Either full_name or both given_name and family_name must be present")
1721
end
1822
end
23+
24+
# Automatically link to a Profile if one exists with a matching ORCID
25+
def link_to_profile_by_orcid
26+
return if orcid.blank?
27+
return if profile_id.present? # Already linked
28+
29+
# Normalize the ORCID for matching - Profile stores it as a full URL
30+
normalized_orcid = orcid.strip
31+
if normalized_orcid =~ OrcidValidator::ORCID_ID_REGEX
32+
normalized_orcid = "#{OrcidValidator::ORCID_PREFIX}#{normalized_orcid}"
33+
end
34+
35+
matching_profile = Profile.find_by(orcid: normalized_orcid)
36+
self.profile = matching_profile if matching_profile.present?
37+
end
1938
end

app/serializers/person_serializer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
class PersonSerializer < ApplicationSerializer
2-
attributes :id, :given_name, :family_name, :full_name, :orcid
2+
attributes :id, :given_name, :family_name, :full_name, :orcid, :profile_id
33

44
# Return display_name for API responses
55
attribute :name do |person|

app/views/materials/show.json.jbuilder

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ json.nodes @material.associated_nodes.collect { |x| { name: x[:name], node_id: x
2121
json.collections @material.collections.collect { |x| { title: x[:title], id: x[:id] } }
2222
json.events @material.events.collect { |x| { title: x[:title], id: x[:id] } }
2323

24-
json.authors @material.authors.collect { |a| { id: a.id, given_name: a.given_name, family_name: a.family_name, full_name: a.full_name, name: a.display_name, orcid: a.orcid } }
25-
json.contributors @material.contributors.collect { |c| { id: c.id, given_name: c.given_name, family_name: c.family_name, full_name: c.full_name, name: c.display_name, orcid: c.orcid } }
24+
json.authors @material.authors.collect { |a| { id: a.id, given_name: a.given_name, family_name: a.family_name, full_name: a.full_name, name: a.display_name, orcid: a.orcid, profile_id: a.profile_id } }
25+
json.contributors @material.contributors.collect { |c| { id: c.id, given_name: c.given_name, family_name: c.family_name, full_name: c.full_name, name: c.display_name, orcid: c.orcid, profile_id: c.profile_id } }
2626

2727
json.external_resources do
2828
@material.external_resources.each do |external_resource|

db/migrate/20251119095244_create_people.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ def change
55
t.string :family_name
66
t.string :full_name
77
t.string :orcid
8+
t.references :profile, null: true, foreign_key: true
89

910
t.timestamps
1011
end

test/models/person_test.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,45 @@ class PersonTest < ActiveSupport::TestCase
3939
person = people(:horace)
4040
assert_respond_to person, :person_links
4141
end
42+
43+
test 'should allow optional profile association' do
44+
person = Person.create(full_name: 'John Doe')
45+
assert person.valid?
46+
assert_nil person.profile
47+
end
48+
49+
test 'should automatically link to profile by orcid on save' do
50+
profile = profiles(:trainer_one_profile)
51+
# The trainer_one_profile has orcid: https://orcid.org/000-0002-1825-0097
52+
person = Person.create(full_name: 'Josiah Carberry', orcid: 'https://orcid.org/000-0002-1825-0097')
53+
assert person.valid?
54+
assert_equal profile, person.profile
55+
end
56+
57+
test 'should automatically link to profile using short orcid format' do
58+
profile = profiles(:trainer_one_profile)
59+
# The trainer_one_profile has orcid: https://orcid.org/000-0002-1825-0097
60+
person = Person.create(full_name: 'Josiah Carberry', orcid: '000-0002-1825-0097')
61+
assert person.valid?
62+
assert_equal profile, person.profile
63+
end
64+
65+
test 'should not link to profile if no matching orcid' do
66+
person = Person.create(full_name: 'John Doe', orcid: '0000-0001-9999-9999')
67+
assert person.valid?
68+
assert_nil person.profile
69+
end
70+
71+
test 'should not override existing profile link' do
72+
profile1 = profiles(:trainer_one_profile)
73+
profile2 = profiles(:admin_trainer_profile)
74+
75+
# First, manually set a profile
76+
person = Person.create(full_name: 'John Doe', profile: profile2)
77+
assert_equal profile2, person.profile
78+
79+
# Even if we update the ORCID to match profile1, it should keep profile2
80+
person.update(orcid: 'https://orcid.org/000-0002-1825-0097')
81+
assert_equal profile2, person.profile
82+
end
4283
end

0 commit comments

Comments
 (0)