Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
8d5710c
Add Author model and migration for authors has_many relationship
Copilot Nov 19, 2025
b399963
Update serializers, views, bioschemas and ingestors for new Author model
Copilot Nov 19, 2025
2f8ce09
Fix tests to work with new Author model
Copilot Nov 19, 2025
ea5876a
Refactor Author to generic Person model with polymorphic PersonLink
Copilot Dec 2, 2025
d360ced
Add custom authors= setter for legacy API backwards compatibility
Copilot Dec 2, 2025
fbd140c
Add HasPeople concern, apply to contributors like authors
Copilot Dec 4, 2025
508211d
Add full_name column, rename first_name/last_name to given_name/famil…
Copilot Dec 5, 2025
4009375
Add legacy first_name/last_name to permitted params for API backwards…
Copilot Dec 5, 2025
d209e06
Add optional belongs_to :profile on Person, auto-link by ORCID on save
Copilot Dec 5, 2025
507208e
Schema
fbacall Dec 5, 2025
6d79f96
Fix missing `end`
fbacall Dec 5, 2025
97f8bcd
DRY up some ORCID handling code
fbacall Dec 5, 2025
5d8acf6
Revert CSV ingestor changes and fix test
fbacall Dec 5, 2025
0921ece
Revert material API change
fbacall Dec 5, 2025
ba745c9
Fix bad ORCIDs in tests
fbacall Dec 11, 2025
d3c818d
Remove the indirection - resources manage their own `Person` objects*
fbacall Feb 11, 2026
d840bac
Fix broken person form (it still looks bad though)
fbacall Feb 11, 2026
b8f36f8
Restore contributors and authors to materials serializer
fbacall Feb 11, 2026
8003356
Test fix
fbacall Feb 12, 2026
c28bada
Just use `full_name` for people
fbacall Feb 20, 2026
8d1af15
Migration to convert people to new table
fbacall Feb 20, 2026
2afa622
Improve person form
fbacall Feb 20, 2026
f8cb2f6
Test fix
fbacall Feb 20, 2026
7435f71
Tidying people form code. Use partial. Refactor JS. Tidying. I18n.
fbacall Feb 25, 2026
46f756e
Richer display of people that have ORCID/profile links
fbacall Feb 25, 2026
60870c4
Only auto-link people to profiles with authenticated ORCIDs
fbacall Feb 25, 2026
f133be7
Remove link to profile if no longer a profile matching the person ORCID
fbacall Feb 25, 2026
7aad8e1
Async job that links `people` to `profiles` when ORCID is authenticat…
fbacall Feb 25, 2026
d4e6747
Autocomplete (name + orcid) in person form
fbacall Mar 2, 2026
8ac96bb
Test fix
fbacall Mar 3, 2026
16718fd
Fix XSS
fbacall Mar 3, 2026
ada5b5a
Migrate Workflow and LearningPath authors/contributors
fbacall Mar 3, 2026
def105b
Deprecate old authors/contributors DB fields
fbacall Mar 3, 2026
22cf330
Prevent needless destruction/creation of `Person` records
fbacall Mar 4, 2026
5ceedaf
Fix assignment of `Person` objects to association
fbacall Mar 4, 2026
bfb09bf
Rename `Person#full_name` -> just `name`
fbacall Mar 5, 2026
fc3acf1
Test author params allows string/structured forms
fbacall Mar 5, 2026
7837e82
Bump RDF extractor lib to parse structured people objects
fbacall Mar 5, 2026
165c28a
Fix string authors no longer working
fbacall Mar 5, 2026
43a99bf
Use both `@id` and `identifier` to serialize person's ORCID in Biosch…
fbacall Mar 5, 2026
6ca7e25
Revert API change
fbacall Mar 5, 2026
9dcb919
Test fix
fbacall Mar 5, 2026
0b7606e
Fix missing associated resources when previewing scraped resources. F…
fbacall Mar 5, 2026
cdc4d39
Merge branch 'master' into author-improvements
fbacall Mar 5, 2026
beda636
Remove generic `people` association and just use role-specific ones
fbacall Mar 5, 2026
873da43
Address Copilot review
fbacall Mar 5, 2026
b099ad2
Fix down migrations
fbacall Mar 5, 2026
b65e7ed
Add space between name and ORCID
fbacall Mar 5, 2026
6decfa1
Merge branch 'author-improvements' of github.com:ElixirTeSS/TeSS into…
fbacall Mar 5, 2026
c2e8085
Remove test for redundant method
fbacall Mar 5, 2026
6f6fbb6
Fix param conversion issue
fbacall Mar 10, 2026
35e95d6
Validate orcid in `has_orcid`
fbacall Mar 10, 2026
d71f98f
Remove leftover `accepts_nested_attributes_for` stuff. Ensure all aut…
fbacall Mar 10, 2026
55e59f9
Index people on name to speed up autocomplete
fbacall Mar 10, 2026
9b3ae7c
Ensure ORCIDs in test code are valid
fbacall Mar 10, 2026
5788cef
Test fix
fbacall Mar 11, 2026
81240a8
Merge branch 'master' into author-improvements
fbacall Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -436,15 +436,15 @@ 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)
actionpack (>= 6.1.0)
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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ document.addEventListener("turbolinks:load", function(e) {

LearningPaths.init();

People.init();

$('.tess-expandable').each(function () {
if (this.dataset.origHeight) {
return;
Expand Down
77 changes: 77 additions & 0 deletions app/assets/javascripts/people.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
10 changes: 0 additions & 10 deletions app/assets/stylesheets/external-resources.scss
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
12 changes: 11 additions & 1 deletion app/assets/stylesheets/forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 9 additions & 1 deletion app/controllers/autocomplete_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 5 additions & 2 deletions app/controllers/learning_paths_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion app/controllers/materials_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: [])
Expand Down
7 changes: 5 additions & 2 deletions app/controllers/workflows_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
23 changes: 23 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 16 additions & 0 deletions app/helpers/materials_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +106 to +119
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The display_people helper iterates over each person and calls person.profile, but the has_person_role association doesn't eager-load profiles. This causes N+1 database queries when a resource has multiple authors or contributors. The has_person_role macro should add includes: :profile to the association scope, or the display_people helper should receive a collection that is already preloaded with profiles.

Copilot uses AI. Check for mistakes.
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
Expand Down
Loading
Loading