Skip to content

Add ROR Support for Institutions #2135

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 75 commits into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
8d0b429
add a new column `ror_id` to the `institutions` table.
whomingbird Dec 12, 2024
0d3ce74
be able to enter ROR ID in UI and save it
whomingbird Dec 12, 2024
c413060
display ROR id as a clickable link
whomingbird Dec 13, 2024
b12edcb
add ror related javascript
whomingbird Dec 13, 2024
29963a1
add ror related css
whomingbird Dec 13, 2024
0dc8740
Add ROR support: Enable users to type an institution name and see a s…
whomingbird Dec 13, 2024
6166a48
validate ROR ID
whomingbird Dec 13, 2024
8783b19
fetch institution metadata via ROR ID
whomingbird Dec 13, 2024
bb9d5b5
add test
whomingbird Dec 13, 2024
dba83bf
add test for institution model
whomingbird Dec 13, 2024
6438f03
Simplify the form view for entering institution data.
whomingbird Dec 13, 2024
e524b00
better UI: add more informative text and related links
whomingbird Dec 16, 2024
6940737
improve the error response message when ROR ID is invalid
whomingbird Dec 17, 2024
76170fe
convert country code to country name
whomingbird Dec 17, 2024
e59942b
mini twist
whomingbird Dec 17, 2024
d5cc2f3
add `ror_id` into institution_serializer
whomingbird Jan 13, 2025
ed349c3
add `ror_id` into institution typehead
whomingbird Jan 13, 2025
52fe079
Add ROR support for project creation: Implement local and remote inst…
whomingbird Jan 13, 2025
9f7b717
Add `ror_id` attribute to Institution Read/Write API
whomingbird Jan 13, 2025
17191c9
update API examples for the new attribute `ror_id` attribute
whomingbird Jan 13, 2025
4b07a17
fix the javascript to enable/disable "submit" button
whomingbird Jan 14, 2025
ef4fb71
add more help text
whomingbird Jan 14, 2025
411c9fe
remove unused javascript
whomingbird Jan 14, 2025
aff30dc
improve CSS
whomingbird Jan 14, 2025
81dba73
more javascript twist
whomingbird Jan 14, 2025
bf3dacb
enable to save ror_id when creating an institution along with a project
whomingbird Jan 14, 2025
ef485f0
disable the input fields when the institution metadata are loaded loc…
whomingbird Jan 17, 2025
ba5f0ed
fix test
whomingbird Jan 17, 2025
60bbdb5
disable the input fields when insitution is selected from locally sav…
whomingbird Jan 23, 2025
ac3807f
remove the typo
whomingbird Jan 27, 2025
9dea67d
add "clear-fields" button when creating institutions
whomingbird Jan 27, 2025
36f9f2b
Rewrite the toggleUserInput method to disable input fields when insti…
whomingbird Jan 27, 2025
5fc578c
ror_id should be unique.
whomingbird Jan 27, 2025
2bf6b5c
when editing institution, if ror ID exists, inputs fields should be d…
whomingbird Jan 27, 2025
a0e76d2
bug fix for the readonly fields
whomingbird Jan 27, 2025
38ae0a6
remove unnecessary field 'institution_name'
whomingbird Jan 27, 2025
50059ae
update checkSubmitButtonEnabled() function
whomingbird Jan 27, 2025
826e822
bug fix #2123
whomingbird Jan 27, 2025
92de4b9
minor javascript fix
whomingbird Jan 29, 2025
5ad6f0d
Merge branch 'main' into add-ror-support
whomingbird Jan 31, 2025
910c6c7
bug fix: the new institution country is not saved
whomingbird Feb 2, 2025
4e3f857
find the existing institutions by title and ror id
whomingbird Feb 4, 2025
d184613
remove "new_institution_reminder"
whomingbird Feb 4, 2025
2043078
bug fix for showing ROR ID link
whomingbird Feb 4, 2025
9c46f93
add tests for administer create request project with an existing inst…
whomingbird Feb 4, 2025
15c2afa
update "request create" project by not_admin and add tests
whomingbird Feb 4, 2025
669fbd9
mini bug
whomingbird Feb 10, 2025
93ffa5e
update according to the PR review
whomingbird Feb 10, 2025
2288a69
use select dropdown for country instead of free text
whomingbird Feb 12, 2025
22a912a
accept "query" as a parameter and dynamically fetch data based on use…
whomingbird Feb 12, 2025
d587587
fix response json
whomingbird Feb 21, 2025
5310c79
remove debug log
whomingbird Feb 21, 2025
e0ce5ce
does not require css
whomingbird Feb 21, 2025
7f7fdec
remove old => syntax
whomingbird Feb 21, 2025
2f7d1e8
add validator for ror_id to check if ID matches any existing ROR orga…
whomingbird Feb 21, 2025
2951e06
fix tests
whomingbird Feb 24, 2025
4dccf95
fix institution api tests
whomingbird Feb 24, 2025
8620b17
fix more institution related tests
whomingbird Feb 24, 2025
04cbde3
use bootstrap sass variables
whomingbird Feb 24, 2025
35adcc5
Refactor ROR API query to route through server to protect user IP
whomingbird Feb 26, 2025
13759f3
rename ROR client
whomingbird Feb 26, 2025
99c8191
rename Ror client
whomingbird Feb 26, 2025
ae94cba
use the routes helper method in case of suburl
whomingbird Mar 11, 2025
61e3ade
Add fetch_ror_details to retrieve all ROR metadata before validation.…
whomingbird Mar 12, 2025
245a202
valid institution when creating it through project
whomingbird Mar 12, 2025
25d6653
add tests for ror client
whomingbird Mar 12, 2025
2a2692f
add tests for invalid id or name
whomingbird Mar 12, 2025
b2871e3
fix tests
whomingbird Mar 12, 2025
2b02352
refactoring ror_mock
whomingbird Mar 13, 2025
b4c82a4
Validate institution before triggering logs or emails in request_crea…
whomingbird Mar 13, 2025
b4f8406
add functional tests for "ror_search"
whomingbird Mar 13, 2025
fce5321
add tests to make sure that fetch_ror_details is called before valida…
whomingbird Mar 13, 2025
3542193
remove duplicated ror_id validation
whomingbird Mar 13, 2025
2491dc5
merge with main
whomingbird Mar 13, 2025
ebcfa33
update schema.rb because Rails 7.2.2.1 does not recognize :size as a …
whomingbird Mar 13, 2025
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
Binary file added app/assets/images/logos/ror_16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,4 @@
//= require copasi/copasi_simulation
//= require plotly-2.27.0.min
//= require extended_metadata_type

//= require institution-ror-typeahead
276 changes: 276 additions & 0 deletions app/assets/javascripts/institution-ror-typeahead.js.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
<% environment.context_class.instance_eval { include Seek::Util.routes } %>

function toggleUserInput(disabled) {
const action = disabled ? 'addClass' : 'removeClass';
const elements = [
'#institution_title',
'#institution_city',
'#institution_ror_id',
'#institution_web_page',
'.tt-input'
];

elements.forEach(selector => {
$j(selector)[action]('institution-input-disable');
$j(selector).prop("readonly", disabled);
$j('#institution_country').prop('disabled', disabled);
});
}

function extractRorId(rorUrl) {
const regex = /https:\/\/ror\.org\/([^\/]+)/;
const match = rorUrl.match(regex);
if (match) {
return match[1];
} else {
return null;
}
}

function fetchRorData(rorId) {

//use /institutions/ror_search endpoint instead of the direct ROR API
var url = '<%= ror_search_institutions_path %>?ror_id=' + encodeURIComponent(rorId);

fetch(url)
.then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error(err.error || "Unknown error occurred");
});
}
return response.json();
})
.then(data => {
if (!data || data.error) {
throw new Error(data.error || "Invalid response from server");
}

$j('#ror-response').html(JSON.stringify(data, null, 4));
$j('#institution_title').val(data.name || 'N/A');
$j('#institution_city').val(data.addresses?.[0]?.city || 'N/A');
$j('#institution_country').val(data.country?.country_code || 'N/A');
$j('#institution_ror_id').val(extractRorId(data.id) || '');
$j('#institution_web_page').val(data.links?.[0] || 'N/A');
$j('#ror-error-message').text('').hide();
$j('#institution_ror_id').removeClass("field_with_errors");
$j("#ror-error-message").closest(".form-group").removeClass("field_with_errors");
toggleUserInput(true);
})
.catch(error => {
$j('#ror-error-message').text(error.message).show();
$j('#institution_ror_id').addClass("field_with_errors");
$j("#ror-error-message").closest(".form-group").addClass("field_with_errors");
});
}

// ROR API source logic
function rorQuerySource(query, processSync, processAsync) {
if (query.length < 4) {
return processAsync([]);
}

//use /institutions/ror_search endpoint instead of the direct ROR API
var url = '<%= ror_search_institutions_path %>?query=' + encodeURIComponent(query);
return $j.ajax({
url: url,
type: 'GET',
dataType: 'json',
success: function (json) {
const orgs = json.items || [];
return processAsync(orgs);
},
error: function (xhr, status, error) {
processAsync([]);
}
});
}

// Template for Local Institution suggestions
function localSuggestionTemplate(data) {
return `
<div>
<strong>${data.text}</strong>
<small>${data.hint || ''}</small>
</div>`;
}

// Template for ROR suggestions
function rorSuggestionTemplate(data) {
var altNames = "";
if (data.aliases.length > 0) {
altNames += data.aliases.join(", ") + ", ";
}
if (data.acronyms.length > 0) {
altNames += data.acronyms.join(", ") + ", ";
}
if (data.labels.length > 0) {
data.labels.forEach(label => {
altNames += label.label + ", ";
});
}
altNames = altNames.replace(/,\s*$/, "");
return `
<div>
<p>
${data.name}<br>
<small>${data.types[0]}, ${data.country.country_code}<br>
<i>${altNames}</i></small>
</p>
</div>`;
}



function initializeLocalInstitutions(query = '', cache = false) {
const url = `<%= typeahead_institutions_path %>.json?q=${encodeURIComponent(query)}`;

return new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('text'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: url,
wildcard: '%QUERY',
cache: cache,
transform: response => response.results
}
});
}

function clearInstitutionFields() {
$j('#institution_title').val('');
$j('#institution_id').val('');
$j('#institution_ror_id').val('');
$j('#institution_city').val('');
$j('#institution_country').val('');
$j('#institution_web_page').val('');
$j('#institution_address').val('');
}


$j(document).ready(function () {
var $j = jQuery.noConflict();

$j('#fetch-ror-data-with-id').on('click', function () {
fetchRorData($j('#institution_ror_id').val());
});

// if the institution title is not selected from the local list or ROR, but entered manually by user
$j('#institution_title').on('change', function () {
const inputValue = $j(this).val();
$j('#institution_title').val(inputValue);
checkSubmitButtonEnabled();
});


$j('#combined_typeahead .typeahead').typeahead(
{
hint: true,
highlight: true,
minLength: 4
},
// First Dataset: Local Institutions
{
name: 'institutions',
display: 'text', // Display the 'text' field in the dropdown
source: function (query, syncResults, asyncResults) {
const bloodhound = initializeLocalInstitutions(query);
bloodhound.search(query, syncResults, asyncResults);
}, // Local data source
templates: {
header: '<div class="league-name">Institutions saved locally</div>',
suggestion: localSuggestionTemplate
}
},
// Second Dataset: Remote ROR Query
{
name: 'ror-query',
limit: 50,
async: true,
source: rorQuerySource,
templates: {
header: '<div class="league-name">Institutions fetched from ROR</div>',
pending: '<div class="empty-message">Fetching from ROR API ...</div>',
suggestion: rorSuggestionTemplate
},
display: function (data) {
return data.name;
},
value: function (data) {
return data.identifier;
}
}
);


$j('#ror_query_name .typeahead').typeahead({
hint: true,
highlight: true,
minLength: 3
},
{
limit: 50,
async: true,
source: rorQuerySource,
templates: {
pending: [
'<div class="empty-message">',
'Fetching list ...',
'</div>'
].join('\n'),
suggestion: rorSuggestionTemplate
},
display: function (data) {
return data.name;
},
value: function (data) {
return data.identifier;
}
});

$j('#combined_typeahead .typeahead').bind('typeahead:select', function (ev, data) {
$j('#combined_typeahead .typeahead').typeahead('close');

if (data.hasOwnProperty("text")) {
$j('#institution_title').val(data.text);
$j('#institution_id').val(data.id);
$j('#institution_ror_id').val(data.ror_id);
$j('#institution_city').val(data.city);
$j('#institution_country').val(data.country);
$j('#institution_web_page').val(data.web_page);
}
else
{
$j('#institution_title').val(data.name);
$j('#institution_ror_id').val(data.id);
$j('#institution_city').val(data.addresses[0]['city']);
$j('#institution_country').val(data.country.country_code);
$j('#institution_ror_id').val(extractRorId(data.id));
$j('#institution_web_page').val(data.links[0]);
}
toggleUserInput(true);
checkSubmitButtonEnabled();
});

$j('#ror_query_name .typeahead').bind('typeahead:select', function (ev, suggestion) {
$j('#ror-response').html(JSON.stringify(suggestion, undefined, 4));
$j('#institution_city').val(suggestion.addresses[0]['city']);
$j('#institution_country').val(suggestion.country.country_code);
$j('#institution_ror_id').val(extractRorId(suggestion.id));
$j('#institution_web_page').val(suggestion.links[0]);
toggleUserInput(true);
});

$j('#clear-fields').on('click', function(event) {
event.preventDefault();
clearInstitutionFields();
toggleUserInput(false);
checkSubmitButtonEnabled();
});


if ($j('#institution_ror_id').val()!== '') {
toggleUserInput(true);
}

});
1 change: 1 addition & 0 deletions app/assets/stylesheets/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@
*= require select2.min
*= require select2.bootstrap.min
*= require linked_extended_metadata
*= require ror-widget
*/
26 changes: 26 additions & 0 deletions app/assets/stylesheets/ror-widget.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import 'bootstrap/variables';

.institution-input-disable {
background-color: $input-bg-disabled !important;
cursor: not-allowed;
}

.tt-menu {
width: 422px;
margin: 12px 0;
padding: 8px 0;
background-color: $input-bg;
border: 1px solid $gray-base;
max-height: 400px;
overflow-y: auto;
border-radius: $border-radius-large;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
}

.league-name {
margin: 0 20px 5px 0;
padding: 3px 6px;
background-color: $btn-default-border;
transition: background-color 0.5s ease-in-out;
}

23 changes: 22 additions & 1 deletion app/controllers/institutions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'ror/client'

class InstitutionsController < ApplicationController
include Seek::IndexPager
include CommonSweepers
Expand Down Expand Up @@ -90,6 +92,7 @@ def typeahead
items = results.map do |institution|
{ id: institution.id,
text: institution.title,
ror_id: institution.ror_id,
web_page: institution.web_page,
city: institution.city,
country:institution.country,
Expand All @@ -114,6 +117,24 @@ def request_all
end
end


def ror_search
client = Ror::Client.new
if params[:query].present?
response = client.query_name(params[:query])
elsif params[:ror_id].present?
response = client.fetch_by_id(params[:ror_id])
else
render json: { error: 'Missing ROR ID' }, status: 400 and return
end

if response.key?(:error)
render json: response, status: 500
else
render json: response
end
end

# request all institutions, but specific to the sharing form which expects an array
def request_all_sharing_form
institution_list = Institution.order(:id).collect{ |institution| [institution.title, institution.id] }
Expand All @@ -127,7 +148,7 @@ def request_all_sharing_form
private

def institution_params
params.require(:institution).permit(:title, :web_page, :address, :city, :country,
params.require(:institution).permit(:title, :web_page, :address, :city, :country, :ror_id,
discussion_links_attributes:[:id, :url, :label, :_destroy])
end

Expand Down
Loading