From b5359d2468a2760df599d54d379ac261e9cfef3c Mon Sep 17 00:00:00 2001 From: robyngit Date: Mon, 14 Oct 2024 14:26:37 -0400 Subject: [PATCH] Add a model for querying CrossRef Issue #2541 --- src/js/models/CrossRefModel.js | 114 ++++++++++++++++++ test/config/tests.json | 1 + .../specs/unit/models/CrossRefModel.spec.js | 37 ++++++ 3 files changed, 152 insertions(+) create mode 100644 src/js/models/CrossRefModel.js create mode 100644 test/js/specs/unit/models/CrossRefModel.spec.js diff --git a/src/js/models/CrossRefModel.js b/src/js/models/CrossRefModel.js new file mode 100644 index 000000000..fc4d2141e --- /dev/null +++ b/src/js/models/CrossRefModel.js @@ -0,0 +1,114 @@ +define(["backbone"], (Backbone) => { + const CACHE_PREFIX = "crossref_"; + /** + * @class CrossRef + * @classdesc Handles querying CrossRef API for metadata about a DOI. + * @classcategory Models + * @augments Backbone.Model + * @constructs + * @augments Backbone.Model + * @since 0.0.0 + */ + const CrossRef = Backbone.Model.extend( + /** @lends CrossRef.prototype */ + { + /** @inheritdoc */ + type: "CrossRef", + + /** + * Defaults for the CrossRef model. + * @type {object} + * @property {string} baseURL - The base URL for the CrossRef API. + * @property {string} email - The email address to use for "polite" + * requests. See https://github.com/CrossRef/rest-api-doc#good-manners--more-reliable-service). + */ + defaults() { + return { + baseURL: + MetacatUI.appModel.get("crossRefAPI") || + "https://api.crossref.org/works/", + email: MetacatUI.appModel.get("emailContact") || "", + }; + }, + + /** @inheritdoc */ + url() { + let doi = this.get("doi"); + if (!doi) return null; + // Make sure the DOI is formatted correctly + doi = MetacatUI.appModel.removeAllDOIPrefixes(doi); + this.set("doi", doi); + const doiStr = encodeURIComponent(doi); + const email = this.get("email"); + const emailStr = email ? `?mailto:${email}` : ""; + const baseURL = this.get("baseURL"); + const url = `${baseURL}${doiStr}${emailStr}`; + return url; + }, + + /** @inheritdoc */ + fetch() { + // first check if there's a cached response + const doi = this.get("doi"); + const cachedResponse = this.getCachedResponse(doi); + if (cachedResponse) { + this.set(cachedResponse); + return; + } + + const url = this.url(); + if (!url) return; + const model = this; + // Make the request using native fetch + fetch(url) + .then((response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); + }) + .then((responseJSON) => { + const parsedData = responseJSON.message; + model.cacheResponse(doi, parsedData); + model.set(parsedData); + model.trigger("sync"); + }) + .catch((error) => { + model.trigger("error", error); + model.set("error", "fetchError"); + model.set("errorMessage", error.message); + }); + }, + + /** + * Cache the response from the CrossRef API + * @param {string} doi The DOI for the response + * @param {object} response The response from the CrossRef API + */ + cacheResponse(doi, response) { + localStorage.setItem(`${CACHE_PREFIX}${doi}`, JSON.stringify(response)); + }, + + /** + * Get the cached response for a DOI + * @param {string} doi The DOI to get the cached response for + * @returns {object|null} The cached response or null if there is no cached response + */ + getCachedResponse(doi) { + const cachedResponse = localStorage.getItem(`${CACHE_PREFIX}${doi}`); + if (!cachedResponse) return null; + return JSON.parse(cachedResponse); + }, + + /** Clear the cache of CrossRef responses */ + clearCache() { + const keysToRemove = Object.keys(localStorage).filter((key) => + key.startsWith(CACHE_PREFIX), + ); + keysToRemove.forEach((key) => localStorage.removeItem(key)); + }, + }, + ); + + return CrossRef; +}); diff --git a/test/config/tests.json b/test/config/tests.json index 6eef2b901..88fe3b759 100644 --- a/test/config/tests.json +++ b/test/config/tests.json @@ -18,6 +18,7 @@ "./js/specs/unit/models/filters/Filter.spec.js", "./js/specs/unit/models/filters/NumericFilter.spec.js", "./js/specs/unit/models/CitationModel.spec.js", + "./js/specs/unit/models/CrossRefModel.spec.js", "./js/specs/unit/collections/ProjectList.spec.js", "./js/specs/unit/collections/DataPackage.spec.js", "./js/specs/unit/models/project/Project.spec.js", diff --git a/test/js/specs/unit/models/CrossRefModel.spec.js b/test/js/specs/unit/models/CrossRefModel.spec.js new file mode 100644 index 000000000..b226ddaa8 --- /dev/null +++ b/test/js/specs/unit/models/CrossRefModel.spec.js @@ -0,0 +1,37 @@ +"use strict"; + +define(["/test/js/specs/shared/clean-state.js", "models/CrossRefModel"], ( + cleanState, + CrossRef, +) => { + const should = chai.should(); + const expect = chai.expect; + + describe("CrossRef Test Suite", () => { + const state = cleanState(() => { + // Example DOI from: + + // Jerrentrup, A., Mueller, T., Glowalla, U., Herder, M., Henrichs, N., + // Neubauer, A., & Schaefer, J. R. (2018). Teaching medicine with the + // help of “Dr. House.” PLoS ONE, 13(3), Article e0193972. + // https://doi.org/10.1371/journal.pone.0193972 + const crossRef = new CrossRef({ + doi: "https://doi.org/10.1371/journal.pone.0193972", + }); + return { crossRef }; + }, beforeEach); + + it("creates a CrossRef instance", () => { + state.crossRef.should.be.instanceof(CrossRef); + }); + + it("forms valid fetch URLs", () => { + const url = state.crossRef.url(); + + url.should.be.a("string"); + url.should.include("https://api.crossref.org/works/"); + url.should.include("10.1371%2Fjournal.pone.0193972"); + url.should.include("?mailto:knb-help@nceas.ucsb.edu"); + }); + }); +});