Skip to content

Commit e2556e3

Browse files
Merge pull request #1437 from datacite/funded-by
Adds funded-by and include-funder-child-organizations URL parameters
2 parents 8bcfc48 + f0bc7a8 commit e2556e3

File tree

9 files changed

+301
-0
lines changed

9 files changed

+301
-0
lines changed

app/controllers/datacite_dois_controller.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ def index
140140
sort: sort,
141141
random: params[:random],
142142
client_type: params[:client_type],
143+
funded_by: params[:funded_by],
144+
include_funder_child_organizations: params[:include_funder_child_organizations],
143145
)
144146
end
145147

@@ -333,6 +335,8 @@ def index
333335
composite: params[:composite],
334336
affiliation: params[:affiliation],
335337
publisher: params[:publisher],
338+
funded_by: params[:funded_by],
339+
include_funder_child_organizations: params[:include_funder_child_organizations],
336340
# The cursor link should be an array of values, but we want to encode it into a single string for the URL
337341
"page[cursor]" =>
338342
page[:cursor] ? make_cursor(results) : nil,

app/models/concerns/rorable.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# frozen_string_literal: true
2+
3+
module Rorable
4+
extend ActiveSupport::Concern
5+
6+
def get_ror_from_crossref_funder_id(funder_id)
7+
funder_id_suffix = funder_id.split("10.13039/").last
8+
FUNDER_TO_ROR[funder_id_suffix]
9+
end
10+
11+
def get_ror_parents(ror_id)
12+
normalized_ror = "https://#{ror_from_url(ror_id)}"
13+
ROR_HIERARCHY[normalized_ror]&.fetch("ancestors", []) || []
14+
end
15+
end

app/models/doi.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class Doi < ApplicationRecord
1414
include Metadatable
1515
include Cacheable
1616
include Dateable
17+
include Rorable
1718

1819
# include helper module for generating random DOI suffixes
1920
include Helpable
@@ -269,6 +270,8 @@ def validate_publisher_obj?(doi)
269270
indexes :organization_id, type: :keyword
270271
indexes :fair_organization_id, type: :keyword
271272
indexes :related_dmp_organization_id, type: :keyword
273+
indexes :funder_rors, type: :keyword
274+
indexes :funder_parent_rors, type: :keyword
272275
indexes :client_id_and_name, type: :keyword
273276
indexes :provider_id_and_name, type: :keyword
274277
indexes :resource_type_id_and_name, type: :keyword
@@ -639,6 +642,8 @@ def as_indexed_json(_options = {})
639642
"organization_id" => organization_id,
640643
"fair_organization_id" => fair_organization_id,
641644
"related_dmp_organization_id" => related_dmp_organization_and_affiliation_id,
645+
"funder_rors" => funder_rors,
646+
"funder_parent_rors" => funder_parent_rors,
642647
"affiliation_id_and_name" => affiliation_id_and_name,
643648
"fair_affiliation_id_and_name" => fair_affiliation_id_and_name,
644649
"media_ids" => media_ids,
@@ -1172,6 +1177,22 @@ def self.query(query, options = {})
11721177
filter << { term: { "client.client_type" => options[:client_type] } } if options[:client_type]
11731178
filter << { term: { "types.resourceTypeGeneral" => "PhysicalObject" } } if options[:client_type] == "igsnCatalog"
11741179

1180+
if options[:funded_by].present?
1181+
normalized_funder = "https://#{ror_from_url(options[:funded_by])}"
1182+
if options[:include_funder_child_organizations] == "true"
1183+
filter << {
1184+
bool: {
1185+
should: [
1186+
{ term: { "funder_rors": normalized_funder } },
1187+
{ term: { "funder_parent_rors": normalized_funder } }
1188+
],
1189+
minimum_should_match: 1
1190+
}
1191+
}
1192+
else
1193+
filter << { term: { "funder_rors": normalized_funder } }
1194+
end
1195+
end
11751196
# match either one of has_affiliation, has_organization, or has_funder
11761197
if options[:has_organization].present?
11771198
should << { term: { "creators.nameIdentifiers.nameIdentifierScheme" => "ROR" } }
@@ -1958,6 +1979,27 @@ def fair_affiliation_id_and_name
19581979
end
19591980
end
19601981

1982+
def funder_rors
1983+
Array.wrap(funding_references).reduce([]) do |sum, f|
1984+
result = if f.is_a?(Hash) && f.fetch("funderIdentifierType", nil) == "ROR" && f.fetch("funderIdentifier", nil).present?
1985+
ror = ror_from_url(f.fetch("funderIdentifier", nil))
1986+
ror.present? ? "https://#{ror}" : nil
1987+
elsif f.is_a?(Hash) && f.fetch("funderIdentifierType", nil) == "Crossref Funder ID" && f.fetch("funderIdentifier", nil).present?
1988+
get_ror_from_crossref_funder_id(f.fetch("funderIdentifier", nil))
1989+
end
1990+
1991+
sum << result if result.present?
1992+
sum
1993+
end.compact.uniq
1994+
end
1995+
1996+
def funder_parent_rors
1997+
Array.wrap(funder_rors).reduce([]) do |sum, ror|
1998+
ancestors = get_ror_parents(ror)
1999+
sum.concat(ancestors) if ancestors.present?
2000+
sum.uniq
2001+
end
2002+
end
19612003

19622004
def prefix
19632005
doi.split("/", 2).first if doi.present?

app/resources/funder_to_ror.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

app/resources/ror_hierarchy.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# frozen_string_literal: true
2+
3+
FUNDER_TO_ROR = JSON.parse(File.read(Rails.root.join("app/resources/funder_to_ror.json"))).freeze
4+
ROR_HIERARCHY = JSON.parse(File.read(Rails.root.join("app/resources/ror_hierarchy.json"))).freeze

spec/concerns/rorable_spec.rb

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# frozen_string_literal: true
2+
3+
require "rails_helper"
4+
5+
describe "Rorable", type: :model do
6+
describe "data is loaded" do
7+
it "loads Crossref Funder Id to ROR mapping" do
8+
expect(FUNDER_TO_ROR).to be_a(Hash)
9+
expect(FUNDER_TO_ROR).not_to be_empty
10+
end
11+
12+
it "loads ROR hierarchy mapping" do
13+
expect(ROR_HIERARCHY).to be_a(Hash)
14+
expect(ROR_HIERARCHY).not_to be_empty
15+
end
16+
end
17+
18+
describe "Crossref Funder ID to ROR mapping" do
19+
let(:doi) { create(:doi) }
20+
21+
it "maps Crossref Funder ID without https://doi.org to ROR" do
22+
funder_id = "10.13039/100010552"
23+
ror_id = doi.get_ror_from_crossref_funder_id(funder_id)
24+
expect(ror_id).to eq("https://ror.org/04ttjf776")
25+
end
26+
27+
it "does not map invalid Crossref Funder ID to ROR" do
28+
funder_id = "10.77777/100010552"
29+
ror_id = doi.get_ror_from_crossref_funder_id(funder_id)
30+
expect(ror_id).to eq(nil)
31+
end
32+
33+
it "maps Crossref Funder ID with https://doi.org to ROR" do
34+
funder_id = "https://doi.org/10.13039/100010552"
35+
ror_id = doi.get_ror_from_crossref_funder_id(funder_id)
36+
expect(ror_id).to eq("https://ror.org/04ttjf776")
37+
end
38+
39+
it "maps Crossref Funder ID with https://doi.org to ROR" do
40+
funder_id = "https://doi.org/10.13039/100010552"
41+
ror_id = doi.get_ror_from_crossref_funder_id(funder_id)
42+
expect(ror_id).to eq("https://ror.org/04ttjf776")
43+
end
44+
end
45+
46+
describe "ROR to ancestor mapping" do
47+
let(:doi) { create(:doi) }
48+
49+
it "maps ROR URL to ancestor" do
50+
ror_id = "https://ror.org/00a0jsq62"
51+
ancestors = doi.get_ror_parents(ror_id)
52+
expect(ancestors).to eq(["https://ror.org/04cw6st05"])
53+
end
54+
55+
it "maps incomplete ROR URL to ancestor" do
56+
ror_id = "ror.org/00a0jsq62"
57+
ancestors = doi.get_ror_parents(ror_id)
58+
expect(ancestors).to eq(["https://ror.org/04cw6st05"])
59+
end
60+
61+
it "maps ROR suffix to ancestor" do
62+
ror_id = "00a0jsq62"
63+
ancestors = doi.get_ror_parents(ror_id)
64+
expect(ancestors).to eq(["https://ror.org/04cw6st05"])
65+
end
66+
67+
it "does not map invalid ROR to ancestor" do
68+
ror_id = "doi.org/00a0jsq62"
69+
ancestors = doi.get_ror_parents(ror_id)
70+
expect(ancestors).to eq([])
71+
end
72+
end
73+
end

spec/models/doi_spec.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2329,4 +2329,38 @@
23292329
})
23302330
end
23312331
end
2332+
2333+
describe "with funding references" do
2334+
let(:doi) { create(:doi,
2335+
funding_references:
2336+
[
2337+
{
2338+
"awardUri": "info:eu-repo/grantAgreement/EC/FP7/282625/",
2339+
"awardTitle": "MOTivational strength of ecosystem services and alternative ways to express the value of BIOdiversity",
2340+
"funderName": "European Commission",
2341+
"awardNumber": "282625",
2342+
"funderIdentifier": "https://doi.org/10.13039/501100000780",
2343+
"funderIdentifierType": "Crossref Funder ID"
2344+
},
2345+
{
2346+
"awardUri": "info:eu-repo/grantAgreement/EC/FP7/284382/",
2347+
"awardTitle": "Institutionalizing global genetic-resource commons. Global Strategies for accessing and using essential public knowledge assets in the life sciences.",
2348+
"funderName": "European Commission",
2349+
"awardNumber": "284382",
2350+
"funderIdentifier": "https://ror.org/00a0jsq62",
2351+
"funderIdentifierType": "ROR"
2352+
}
2353+
]
2354+
) }
2355+
2356+
it "has normalized funding references in funder_rors" do
2357+
expect(doi.funder_rors).to eq(["https://ror.org/00k4n6c32", "https://ror.org/00a0jsq62"])
2358+
expect(doi.as_indexed_json["funder_rors"]).to eq(["https://ror.org/00k4n6c32", "https://ror.org/00a0jsq62"])
2359+
end
2360+
2361+
it "has funder ancestor ROR in funder_parent_rors" do
2362+
expect(doi.funder_parent_rors).to eq(["https://ror.org/019w4f821", "https://ror.org/04cw6st05"])
2363+
expect(doi.as_indexed_json["funder_parent_rors"]).to eq(["https://ror.org/019w4f821", "https://ror.org/04cw6st05"])
2364+
end
2365+
end
23322366
end

spec/requests/datacite_dois/datacite_dois_spec.rb

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,132 @@ def clear_doi_index
653653
end
654654
end
655655

656+
describe "GET /dois with funded_by filter", prefix_pool_size: 3 do
657+
let!(:dois) { create_list(:doi, 10, client: client, aasm_state: "findable", version_info: "testtag") }
658+
let!(:funded_by_ec_dois) do
659+
create_list(:doi, 5, client: client, aasm_state: "findable", funding_references:
660+
[
661+
{
662+
"awardUri": "info:eu-repo/grantAgreement/EC/FP7/282625/",
663+
"awardTitle": "MOTivational strength of ecosystem services and alternative ways to express the value of BIOdiversity",
664+
"funderName": "European Commission",
665+
"awardNumber": "282625",
666+
"funderIdentifier": "https://doi.org/10.13039/501100000780",
667+
"funderIdentifierType": "Crossref Funder ID"
668+
}
669+
])
670+
end
671+
672+
let!(:funded_by_ec_crossref_funder_id_descendant_dois) do
673+
create_list(:doi, 2, client: client, aasm_state: "findable", funding_references:
674+
[
675+
{
676+
"awardUri": "info:eu-repo/grantAgreement/EC/FP7/282625/",
677+
"awardTitle": "MOTivational strength of ecosystem services and alternative ways to express the value of BIOdiversity",
678+
"funderName": "European Commission",
679+
"awardNumber": "282625",
680+
"funderIdentifier": "https://doi.org/10.13039/501100000781",
681+
"funderIdentifierType": "Crossref Funder ID"
682+
}
683+
])
684+
end
685+
let!(:funded_by_ec_ror_descendant_dois) do
686+
create_list(:doi, 4, client: client, aasm_state: "findable", funding_references:
687+
[
688+
{
689+
"awardUri": "info:eu-repo/grantAgreement/EC/FP7/282625/",
690+
"awardTitle": "MOTivational strength of ecosystem services and alternative ways to express the value of BIOdiversity",
691+
"funderName": "European Commission",
692+
"awardNumber": "282625",
693+
"funderIdentifier": "ror.org/05mkt9r11",
694+
"funderIdentifierType": "ROR"
695+
}
696+
])
697+
end
698+
let!(:funded_by_lshtm_dois) do
699+
create_list(:doi, 2, client: client, aasm_state: "findable", funding_references:
700+
[
701+
{
702+
"awardUri": "info:eu-repo/grantAgreement/EC/FP7/284382/",
703+
"awardTitle": "Institutionalizing global genetic-resource commons. Global Strategies for accessing and using essential public knowledge assets in the life sciences.",
704+
"funderName": "London School of Hygiene & Tropical Medicine",
705+
"awardNumber": "284382",
706+
"funderIdentifier": "https://ror.org/00a0jsq62",
707+
"funderIdentifierType": "ROR"
708+
}
709+
])
710+
end
711+
712+
before do
713+
clear_doi_index
714+
import_doi_index
715+
end
716+
717+
it "filters by funder when funded_by is set with Crossref Funder ID funderIdentifier", vcr: true do
718+
get "/dois?funded-by=https://ror.org/00k4n6c32", nil, headers
719+
720+
expect(last_response.status).to eq(200)
721+
expect(json["data"].size).to eq(5)
722+
expect(json["data"].map { |d| d["attributes"]["doi"] }).to include(funded_by_ec_dois.first.doi.downcase)
723+
end
724+
725+
it "filters by funder when query string is set with Crossref Funder ID funderIdentifier", vcr: true do
726+
get "/dois?query=funder_rors:\"https://ror.org/00k4n6c32\"", nil, headers
727+
728+
expect(last_response.status).to eq(200)
729+
expect(json["data"].size).to eq(5)
730+
expect(json["data"].map { |d| d["attributes"]["doi"] }).to include(funded_by_ec_dois.first.doi.downcase)
731+
end
732+
733+
it "filters by funder when funded_by is set with ROR funderIdentifier", vcr: true do
734+
get "/dois?funded-by=https://ror.org/00a0jsq62", nil, headers
735+
736+
expect(last_response.status).to eq(200)
737+
expect(json["data"].size).to eq(2)
738+
expect(json["data"].map { |d| d["attributes"]["doi"] }).to include(funded_by_lshtm_dois.first.doi.downcase)
739+
end
740+
741+
it "filters by funder and funder child orgs when funded_by is set with Crossref Funder ID funderIdentifier and include_funder_child_organizations is true", vcr: true do
742+
get "/dois?funded-by=https://ror.org/00k4n6c32&include-funder-child-organizations=true", nil, headers
743+
744+
expect(last_response.status).to eq(200)
745+
expect(json["data"].size).to eq(11)
746+
expect(json["data"].map { |d| d["attributes"]["doi"] }).to include(funded_by_ec_dois.first.doi.downcase)
747+
expect(json["data"].map { |d| d["attributes"]["doi"] }).to include(funded_by_ec_crossref_funder_id_descendant_dois.first.doi.downcase)
748+
expect(json["data"].map { |d| d["attributes"]["doi"] }).to include(funded_by_ec_ror_descendant_dois.first.doi.downcase)
749+
end
750+
751+
it "filters by funder and funder child orgs when query string is set with Crossref Funder ID funderIdentifier and child organizations are included", vcr: true do
752+
get "/dois?query=funder_rors:\"https://ror.org/00k4n6c32\" OR funder_parent_rors:\"https://ror.org/00k4n6c32\"", nil, headers
753+
754+
expect(last_response.status).to eq(200)
755+
expect(json["data"].size).to eq(11)
756+
expect(json["data"].map { |d| d["attributes"]["doi"] }).to include(funded_by_ec_dois.first.doi.downcase)
757+
expect(json["data"].map { |d| d["attributes"]["doi"] }).to include(funded_by_ec_crossref_funder_id_descendant_dois.first.doi.downcase)
758+
expect(json["data"].map { |d| d["attributes"]["doi"] }).to include(funded_by_ec_ror_descendant_dois.first.doi.downcase)
759+
end
760+
761+
it "filters by funder when entered ROR is a parent of entered RORs or Crossref Funder IDs but isn't included in metadata", vcr: true do
762+
get "/dois?funded-by=https://ror.org/019w4f821&include-funder-child-organizations=true", nil, headers
763+
764+
expect(last_response.status).to eq(200)
765+
expect(json["data"].size).to eq(11)
766+
expect(json["data"].map { |d| d["attributes"]["doi"] }).to include(funded_by_ec_dois.first.doi.downcase)
767+
expect(json["data"].map { |d| d["attributes"]["doi"] }).to include(funded_by_ec_crossref_funder_id_descendant_dois.first.doi.downcase)
768+
expect(json["data"].map { |d| d["attributes"]["doi"] }).to include(funded_by_ec_ror_descendant_dois.first.doi.downcase)
769+
end
770+
771+
it "filters by funder when entered ROR in query is a parent of entered RORs or Crossref Funder IDs but isn't included in metadata", vcr: true do
772+
get "/dois?query=funder_rors:\"https://ror.org/019w4f821\" OR funder_parent_rors:\"https://ror.org/019w4f821\"", nil, headers
773+
774+
expect(last_response.status).to eq(200)
775+
expect(json["data"].size).to eq(11)
776+
expect(json["data"].map { |d| d["attributes"]["doi"] }).to include(funded_by_ec_dois.first.doi.downcase)
777+
expect(json["data"].map { |d| d["attributes"]["doi"] }).to include(funded_by_ec_crossref_funder_id_descendant_dois.first.doi.downcase)
778+
expect(json["data"].map { |d| d["attributes"]["doi"] }).to include(funded_by_ec_ror_descendant_dois.first.doi.downcase)
779+
end
780+
end
781+
656782
describe "GET /dois with resource-type-id filter", elasticsearch: false, prefix_pool_size: 1 do
657783
let!(:instrument_doi) { create(:doi, client: client, aasm_state: "findable", types: { "resourceTypeGeneral": "Instrument" }) }
658784
let!(:study_registration_doi) { create(:doi, client: client, aasm_state: "findable", types: { "resourceTypeGeneral": "StudyRegistration" }) }
@@ -726,6 +852,7 @@ def clear_doi_index
726852
let!(:dois) { create_list(:doi, 3, aasm_state: "findable") }
727853

728854
before do
855+
clear_doi_index
729856
import_doi_index
730857
end
731858

0 commit comments

Comments
 (0)